20201027のReactに関する記事は10件です。

オブジェクトを個々の属性に展開する ...演算子の使い方

オブジェクトを個々の属性に展開する ...演算子

MyAttributes.js

import React, {Component} from 'react';

export default class MyAttributes extends Component {
  render(){
    return(
      <ul>
        <li>名前:{this.props.name}</li>
        <li>年齢:{this.props.age}</li>
        <li>性別:{this.props.gender}</li>
      </ul>
    );
  }
}

index.js

const attr = {
  name: "田中",
  age: "31",
  gender: "male",

ReactDOM.render(
  <MyAttrbutes {...atr} />,
  document.getElementById("root");
);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript+Reactでpropsを使ってコンポーネントにデータを渡す流れ

Reactでコンポーネントにデータを渡す流れのメモ。
ソースはこちら

サンプル概要

  • 書籍の一覧を表示する
  • 表示するのは、書籍名と著者名
  • 書籍のデータは決め打ちとする

コンポーネント概要

  • App.tsx
  • Book.tsx・・・書籍情報(書籍名と著者名)を表すコンポーネント
  • BookList.tsx・・・Bookコンポーネントを一覧で表示するコンポーネント

propsを使ってコンポーネントにデータを渡す

  • コンポーネントにデータを渡すためのpropsに使う型(Book.tsx)を定義する
Book.tsx
export type Book = {
    id: number;
    name: string;
    author: string;
}

  • BookListコンポーネントのpropsの型として、上記で作成した型を指定する
BookList.tsx
import React, { FC } from 'react';
import { Book } from './Book';
import { Item } from 'semantic-ui-react';

type bookProps = {
    books: Book[];
}

const BookList: FC<bookProps> = (bookProps) => {
    const { books } = bookProps;

    return (
        <Item.Group>
            {books.map((book) => (
                <Item key={book.id}>
                    <Item.Content>
                        <Item.Header>{book.name}</Item.Header>
                        <Item.Meta>{book.author}</Item.Meta>
                    </Item.Content>
                </Item>
            ))}
        </Item.Group>
    );
};

export default BookList;

  • Appコンポーネントにて書籍情報をBookListコンポーネントに渡す
App.tsx
import React, { FC } from 'react';
import { Book } from './Book';
import BookList from './BookList';
import './App.css';

const App: FC = () => {
  const books: Book[] = [
    {
      id: 1,
      name: '独学大全',
      author: '読書猿'
    },
    {
      id: 2,
      name: '数学ガールの秘密ノート/場合の数',
      author: '結城浩'
    },
    {
      id: 3,
      name: 'アジャイルサムライーー達人開発者への道',
      author: 'Jonathan Ramusson, 西村直人'
    }
  ];

  return (
    <div className="container">
      <header>
        <h1>書籍一覧</h1>
      </header>
      <BookList books={books} />
    </div>
  );
}

export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react + typescriptでローカルから画像を選択してプレビュー画面を表示させる方法

やりたいこと

ローカルにある画像ファイルを選択して、プレビューを表示させたい!

コード

export const UploadImage: React.FC = () = {
    const imageHander = (event: ChangeEvent<HTMLInputElement>) => {
        if (event.target.files === null) {
            return;
        }
        const file = event.target.files[0];
        if (file === null) {
            return;
        }
        let imgTag = document.getElementById("preview") as HTMLImageElement;
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => {
            const result: string = reader.result as string;
            imgTag.src = result;
        }
    };
    return (
        <div>
            <input type="file" accept="image/png, image/jpeg, image/gif" onChange={imageHander} />
            <img id="preview" src=""></img>
       </div>
    );
}

解説

let imgTag = document.getElementById("preview") as HTMLImageElement;

ここでは、HTMLImageElementに型を変換しています。これは、デフォルトではdocument.getElementById("preview")の型がHTMLElement | nullとなっており、このままではsrcパラメータの設定ができないからです。

reader.onload = () => {
    const result: string = reader.result as string;
    imgTag.src = result;
}

ここではreader.resultの型をstringに変換しています。これは、デフォルトではreader.resultの型がstring | ArrayBuffer | nullとなっており、新たなsrcの値として与えることができないためです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-transition-group

react-transition-group

react-transition-groupドキュメント

react-transition-groupはReactでCSSアニメーションを扱う為のライブラリです。
見栄えのするモーション自体を提供してくれるわけではなく、CSSを適用するタイミングを提供してくれるので、自分でアニメーションのCSSを書いてアニメーションさせます。

下記の4つのコンポーネントが提供されます。

  • Transition
  • CSSTransition
  • SwitchTransition
  • TransitionGroup

今回はcreate-react-appを使い、CSS(SCSS)はCSSModulesを使っていきます。
※レンダリング回数削減などパフォーマンス面については扱いません。

インストール

terminal
//create-react-appのインストール
npx create-react-app プロジェクト名

//プロジェクト直下に移動
cd プロジェクト名

//node-sassのインストール(scssではなくcssを使う場合は不要です。)
npm i -D node-sass

//react-transition-groupのインストール
npm i -S react-transition-group

//起動
npm start

src/App.jsの不要な部分を削除します。

src/App/js
import React from "react";
import "./App.css";

function App() {
  return (
    <div className="App">

  {/* ここにこれから作るコンポーネントを配置 */}

    </div>
  );
}

export default App;

Transition

シンプルなトランジション

どのタイミングでinと状態が変化しているか視覚化しています。

transition.gif

src\components\singleTransition\SingleTransition.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./singleTransition.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
   exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};

//SingleTransitionコンポーネント
const SingleTransition = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (

    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>

          <Transition in={mount} timeout={1000} >

            {(state) =>
              <div className={Style.circleShape} style={transitionStyle[state]} >
                <div>
                  <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                  <p className={Style.circleText}> {state}</p>
              </div>
             </div>}

          </Transition>

        </div>

      </div>

    </div>

  );

}

export default SingleTransition;
src\components\singleTransition\singleTransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import SingleTransition from "./components/singleTransition/SingleTransition";

function App() {
  return (
    <div className="App">
     <ChainTransition />
    </div>
  );
}

export default App;

classNameでstateを含むクラス名を指定することで、transitionの状態によって独自のクラスを適用する事もできます

必須のProps

in

inの状態 結果
inがtrueになる マウント開始
inがfalseになる アンマウント開始

timeout

entering、exitingのトランジションを使う場合で、addEndListenerを設定しない場合は必須です。

timeoutの指定による状態の変化
状態 初期 timeoutで指定した時間経過後
マウント時 entering entered
アンマウント時 exiting exited

各transitionに個別にタイムアウトを指定することもできます。

sample
timeout={{
  appear: 500,
  enter: 300,
  exit: 500,
}}

addEndListener

entering、exitingのトランジションを使う場合、timeoutを設定しない場合は必須です。
カスタムのtransition終了トリガーを追加して、動的にtimeout時間を設定したい場合に使用します。

sample
<Transition in={mount} {...callBacks} timeout={1000} >
//↓
<Transition in={mount} {...callBacks} addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}>

inがtrueになる度に追加されるので、毎回doneを呼ぶタイミングでイベントの解除が必要です。

timeoutとaddEndListener

timeoutoとaddEndListenerをどちらも指定しないとコンポーネントのトランジションはenteredとexitedだけを繰り返します。
両方指定した場合はaddEndListenerはフォールバックとして使用されます。

4つの状態

inとtimeoutの組み合わせで、コンポーネントに4つの状態が提供されます。
使用しないものについては、省略可能です。

  • entering
  • enterd
  • exiting
  • exited

enteringとexitingのtransitionの時間の長さについては、通常コンポーネントのtimeoutの値とそろえますが、あえて違う値にすることもできます。

inとtimeout以外のProps

otherprops.gif

src\components\otherProps\OtherProps.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./otherProps.module.scss";

//アニメーションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
  exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};


const OtherProps = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (
    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>
          <p>Normal</p>
          <Transition in={mount} timeout={1000} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>mountOnEnter</p>
          <Transition in={mount} timeout={1000} mountOnEnter >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>unmountOnExit</p>
          <Transition in={mount} timeout={1000} unmountOnExit >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
              </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>enter=false</p>
          <Transition in={mount} timeout={1000} enter={false} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>exit=false</p>
          <Transition in={mount} timeout={1000} exit={false} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>nodeRef</p>
          <Transition in={mount} timeout={1000} nodeRef >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

      </div>

    </div>

  );

}

export default OtherProps;
src\components\otherProps\otherProps.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import OtherProps from "./components/otherProps/OtherProps";

function App() {
  return (
    <div className="App">

     <ChainTransition />
    </div>
  );
}

export default App;

in、timeout以外のProps

Props 未指定時(暗黙的に指定されている) 未指定時から変更する場合 変更時内容
enter enter、enter={true} enter={false} enteringにならない
exit exit、exit={true} exit={false} exitingにならない
mountOnEnter mountOnEnter={false} mountOnEnter、mountOnEnter={true} 遅延マウント(初回のみ)
unmountOnExit unmountOnExit={false} unmountOnExit、unmountOnExit={true} exitedでアンマウント
nodRef nodeRef={false} nodeRef、nodeRef={true} entering、exitingにならない
appear appear={false} appear={true} appearの動作(in=trueと一緒に指定)

appear

各コンポーネントのinの初期値をtrue、appear=trueにします。

変更箇所のみ

src\components\otherProps\OtherProps.js
//mountの初期値をtrueにして、in=trueにする
const [mount, setMount] = useState(true);

//各コンポーネントにappear={true}を追加
<Transition in={mount} timeout={1000} {...callBacksNormal} appear={true} >

コールバック

状態の変化時に処理を行う事ができます。

sample
//状態変化時のコールバック
const callBacks = {
  onEnter: () => console.log("enterです"),
  onEntered: () => console.("enteredです"),
  onExit: () => console.log("exitです"),
  onExited: () => console.log("exitedです"),
};


//Transitionに{...callBacks}を追加
<Transition in={mount} {...callBacks} timeout={1000} >

Transitionを連鎖させる

chain.gif

src\components\chainTransition\ChainTransition.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./chaintransition.module.scss";

//アニメーションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
  exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};

//ChainTransitionコンポーネント
const ChainTransition = () => {

  //マウントの状態を管理
  const [firstCircle, setFirstCircle] = useState(false);
  const [secondCircle, setSecondCircle] = useState(false);
  const [thirdCircle, setThirdCircle] = useState(false);
  const [fourthCircle, setFourthCircle] = useState(false);
  const [fifthCircle, setFifthCircle] = useState(true);
  const [sixthCircle, setSixthCircle] = useState(true);
  const [seventhCircle, setSeventhCircle] = useState(true);

  //マウントのオンオフを切り替える
  const changer = () => {
    setFirstCircle(!firstCircle);
  };

  const callBacks = {

    onEnter: () => {
      setSecondCircle(true);
    },
    onEntering: () =>{
      setThirdCircle(true);
    },
    onEntered: () => {
      setFourthCircle(true);
    },
    onExit: () => {
      setFifthCircle(false);
    },
    onExiting: () => {
      setSixthCircle(false);
    },
    onExited: () => {
      setSeventhCircle(false);
    },

  };

  return (
    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>
        <p>(trigger)</p>
        <Transition in={firstCircle} timeout={1000} {...callBacks}>
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {firstCircle ? "in=true" : "in=false"}</p>
              <p className={Style.circleText}> {state}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEnter</p>
        <Transition in={secondCircle} timeout={1000}  >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {secondCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEntering</p>
        <Transition in={thirdCircle} timeout={1000}>
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {thirdCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEntered</p>
        <Transition in={fourthCircle} timeout={1000} >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {fourthCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onExit</p>
        <Transition in={fifthCircle} timeout={1000}  >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {fifthCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onExiting</p>
        <Transition in={fifthCircle} timeout={1000} >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {sixthCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>


      <div className={Style.circleMember}>
        <p>onExited</p>
          <Transition in={seventhCircle} timeout={1000} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {seventhCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
          </Transition>
      </div>

    </div>

  </div>

  );
}

export default ChainTransition;
src\components\chainTransition\chaintransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  color: red;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import ChainTransition from "./components/chainTransition/ChainTransition";

function App() {
  return (
    <div className="App">

     <ChainTransition />
    </div>
  );
}

export default App;

コールバックのタイミング

nodeRef Propが渡されると、コールバックを使ってもnodeは渡されません。

コールバックの種類 適用タイミング
onEnter entering適用前
onEntering entering適用時
onEntered entered適用時
onExit exiting適用前
onExiting exiting適用時
onExited exited適用時

CSSTransition

Transitionとの大きな違いは、親要素の名前がになっていることと、CSSTransition用のPropsとしてclassNameが使える事です。

csstransition.gif

src\components\singleCSSTransition\SingleCSSTransition.js
import React, { useState } from "react";
import { CSSTransition } from "react-transition-group";
import Style from "./singleCSSTransition.module.scss";


//SingleCSSTransitionコンポーネント
const SingleCSSTransition = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (

    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup} >

        <div className={Style.circleMember} >

          <CSSTransition
            in={mount}
            timeout={1000}
            classNames={{
              appear:Style.testAppear,
              appearActive:Style.testAppearActive,
              appearDone:Style.testAppearDone,
              enter:Style.testEnter,
              enterActive: Style.testEnterActive,
              enterDone:Style.testEnterDone,
              exit:Style.testExit,
              exitActive: Style.testExitActive,
              exitDone: Style.testExitDone,
            }}
            >

            {(state) =>
              <div className={Style.circleShape} >
                <div>
                  <p className={Style.circleText} > {mount ? "in=true" : "in=false"}</p>
                  <p className={Style.circleText} > {state}</p>

              </div>
             </div>}

          </CSSTransition>

        </div>

      </div>

    </div>

  );

}

export default SingleCSSTransition;
src\components\singleCSSTransition\singleCSSTransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}

//apperaの最初のフレーム瞬間の状態
.testAppear {
  transition: all 1s ease;
  border-radius: 10px;
  transform: rotateZ(30deg) scale(2);
  opacity: 0;
}

//.testAppearの直後の状態
.testAppearActive {
}

//appearの最終フレームの状態
.testAppearDone {
  transition: all 1s ease;
  border-radius: 50px;
  opacity: 1;
}

//enter中の最初のフレーム瞬間の状態
.testEnter {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: red;
}
//enter中の最終フレームの状態
.testEnterActive {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: red;
}

//enter完了の状態
.testEnterDone {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: green;
}

//exitの初期状態
.testExit {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: green;
}

//exit中の最終フレームの状態
.testExitActive {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: blue;
}
//exit完了
.testExitDone {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: gray;
}
src\App.js
import React from "react";

import SingleCSSTransition from "./components/singleCSSTransition/SingleCSSTransition";

function App() {
  return (
    <div className="App">

     <SingleCSSTransition />
    </div>
  );
}

export default App;

className

CSSTransitionのclassName Propsでは、各状態にクラス名を付けられます。

sample
<CSSTransition
  classNames={{
    enter:Style.testEnter,
    enterActive: Style.testEnterActive,
    enterDone:Style.testEnterDone,
    exit:Style.testExit,
    exitActive: Style.testExitActive,
    exitDone: Style.testExitDone,
}}>  

今回はCSSModulesを採用しているので、使用する各状態用用のクラスに個別に名前をつける必要がありますが、他の方法でCSSを指定している場合接頭辞を指定すれば、自動的にクラス名が生成されます。
※上記のように任意のクラス名に変更することも可能です。

table:接頭辞をtestにした場合

- active done
test-appear test-appear-active test-appear-done
test-enter test-enter-active test-enter-done
test-enter test-exit-active test-exit-done

※appearクラスの追加タイミングを追ってみると、enterクラスと同時に適用されてしまうので使い方には注意が必要です。

appear

コンポーネントのinの初期値をtrue、appear=trueにします。

変更箇所のみ記載

src\components\singleCSSTransition\SingleCSSTransition.js
//mountの初期値をtrueにしてin=trueにする
const [mount, setMount] = useState(true);

//CSSTransitonにappear={true}を追加
<CSSTransition
  in={mount}
  {...callBacks}
  timeout={1000}
  classNames={{
    appear:Style.testAppear,
    appearActive:Style.testAppearActive,
    appearDone:Style.testAppearDone,
    enter:Style.testEnter,
    enterActive: Style.testEnterActive,
    enterDone:Style.testEnterDone,
    exit:Style.testExit,
    exitActive: Style.testExitActive,
    exitDone: Style.testExitDone,
  }}
  appear={true}
>

SwitchTransition

SwitchTransitionでTransition、CSSTransitionを囲みます。
SwitchTransition独自のPropsとしてmodeがあります。

switch.gif

src\components\transitionSwitching\TransitionSwiching.js
import React, { useState } from "react";
import { SwitchTransition, Transition } from "react-transition-group";
import Style from "./transitionSwitching.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 0.5s ease",
    transform: "translateX(-150px) ",
    backgroundColor:"red",
    opacity:"0",
  },
  entered: {
    transition: "all 0.5s ease",
    transform: "translateX(150px)",
    backgroundColor:"green",
    opacity:"1",
  },
  exiting: {
    transition: "all 0.5s ease",
    transform: "translateX(-150px)",
    backgroundColor: "blue",
    opacity:"1",
  },
  exited: {
   transition: "all 0.5s ease",
    transform: "translateX(150px)",
    backgroundColor: "gray",
    opacity:"0",
  },
};

const TransitionSwitch=()=> {
  const [name, setName] = useState(false);

  return (
    <div>
      <button onClick={() => setName(!name)}>Switching</button>
      <div className={Style.squareWrapper}>
      <SwitchTransition mode="in-out">

        <Transition
          key={name ? "aaa" : "bbb"}
          timeout={500}
          unmountOnExit
          mountOnEnter
          >

          {state => <div state={state} style={transitionStyle[state]} className={Style.square}>
  {name ? <p>AAA</p> : <p>BBB</p>}
          </div>}

        </Transition>

      </SwitchTransition>
      </div>

    </div>

  );
}

export default TransitionSwitch;
src\components\transitionSwitching\transitionSwitching.module.scss
/* 簡易リセット */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* スクロールバー常時表示 */
html {
  overflow-y: scroll;
}
.squareWrapper {
  margin-top: 100px;
  position: relative;
}
.square {
  background-color: blue;
  display: block;
  width: 200px;
  padding: 10px 20px;
  margin: 0 auto;
  color: #fff;
  text-align: center;
  position: absolute;
  left: calc(50% - 100px);
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";

import TransitionSwitching from "./components/transitionSwitching/TransitionSwiching";

function App() {
  return (
    <div className="App">
     <TransitionSwitching />
    </div>
  );
}

export default App;

mode

mode 内容
out-in 先に現在の要素がアウトし、完了後に新しい要素がインする
in-out 先に新しい要素がインし、完了後に現在の要素がアウトする

TransitionGroupとSwitchTransitionの使い分け

古い子要素のoutと新しい子要素のinを同時に行う場合は、TransitionGroupを使用します。

TransitionGroup

TransitionGroup は、Transition または CSSTransition のリストを管理する為のコンポーネントです。

TransitionGroupでTransitionやCSSTransitionをラップします。
各TransitionやCSSTransitionにはinは不要で、代わりにユニークなKeyを設定します。

group.gif

上記サンプルを操作する場合はこちら

src\components\transitionList\TransitionList.js
import React, { useState,useRef } from "react";
import {TransitionGroup,Transition} from 'react-transition-group';
import Style from "./transitionList.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {

  entering: {
    transition: "all 0.2s ease",
    opacity:"0"
  },
  entered: {
    transition: "all 0.2s ease",
    opacity:"1"
  },
  exiting: {
    transition: "all 0.2s ease",
    opacity:"0"
  },
  exited: {
    transition: "all 0.2s ease",
    opacity:"0"
  },

};

//TransitionGroupコンポーネント
const TransitionList = () => {

  //inputに入力中の文字
  const [inputting,setInputting]=useState("");

  //input関連付け用のref
  const inputRef =useRef();

  //最後のID番号管理用
  const [lastId,setLastId]=useState(3);

  //初期アイテムリスト
  const initialList=[
    {id:0,word:"React"},
    {id:1,word:"Hooks"},
    {id:2,word:"Transition"},
  ];

  //アイテムリストに初期アイテムをセット
  const [items,setItems]=useState(initialList);

  //アイテムの追加処理
  const adder = () => {
    if(inputting){
      setItems(
      items=>[
        ...items,
       {id:lastId,
        word:inputting}
      ]
    )};

    //IDのインクリメント
    setLastId(prevId=>prevId+1);

    //inputのクリア
    setInputting("");
    inputRef.current.value="";
  };

  //リセット
  const reseter=()=>{
    setInputting("");
    inputRef.current.value="";
    setItems(initialList);
  }

  //form送信防止
  const stopSubmit=(e)=>{
    e.preventDefault();
  }

  return (

    <div className={Style.wrapper}>

      <form action=""
        onSubmit={stopSubmit}
        className={Style.controller}
      >

        <div className={Style.controllerAddGroup}>
          <input
            className={Style.controllerInput}
            ref={inputRef}
            type="text"
            onChange={
              (e)=>setInputting(e.target.value)
            }
          />

          <button
            className={Style.button}
            onClick={adder}
            disabled={!inputting ? true:false}
          >追加</button>
        </div>

        <button
          className={Style.button}
          onClick={reseter}>
          初期化
        </button>

      </form>

      <div className={Style.cardGroup}>

        <TransitionGroup className={Style.cardInner}>

          {items.map(({id,word})=>(

            <Transition key={id} timeout={200} >
              {(state) =>
                <div className={Style.cardShape} style={transitionStyle[state]} >
                  <button
                    className={Style.button}
                     onClick={() =>
                      setItems(items =>
                        items.filter(item => item.id !== id)
                      )
                    }
                  >
                  削除
                  </button>

                  <p className={Style.cardWord}> {word}</p>
                  <p className={Style.cardTransition}> {state}</p>
             </div>}

          </Transition>
              )
            )
          }
        </TransitionGroup>

      </div>

    </div>

  );

}

export default TransitionList;

src\components\transitionList\transitionList.module.scss
/* 簡易リセット */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* スクロールバー常時表示 */
html {
  overflow-y: scroll;
}

@media screen and (min-width: 651px) {
  .wrapper {
    margin: 0 auto;
    max-width: 600px;
  }

  .controller {
    background-color: #ffebee;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    margin-top: 40px;
    padding: 20px;
  }

  .controllerAddGroup {
    display: flex;
  }

  .controllerInput {
    border: none;
    padding: 10px;
    background-color: #ffcdd2;
    transition: all 0.5s ease;

    &:focus {
      background-color: #ffffff;
      outline: none;
    }
  }

  .button {
    appearance: none;
    background-color: #ec407a;
    border: none;
    color: #ffffff;
    cursor: pointer;
    font-family: inherit;
    font-size: 1rem;
    margin: 0;
    outline: none;
    padding: 10px 20px;
    transition: all 0.5s ease;
    width: 100px;

    &:hover {
      background-color: #ad1457;
    }

    &:disabled {
      opacity: 0;
      cursor: default;
    }
  }

  .cardGroup {
    display: flex;
    justify-content: space-between;
    margin-top: 40px;
    max-width: 1000px;
  }

  .cardInner {
    width: 100%;
  }

  .cardShape {
    background-color: #e3f2fd;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    padding: 20px;
    max-width: 600px;

    & + .cardShape {
      margin-top: 20px;
    }
  }

  .cardWord {
    color: #212121;
    display: inline;
    font-size: 2rem;
    font-weight: bold;
    line-height: 1.2;
    overflow: hidden;
    padding: 0 20px;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    width: 400px;
  }

  .cardTransition {
    background-color: #1e88e5;
    color: #ffffff;
    border-radius: 4px;
    padding: 10px;
  }
}

@media screen and (max-width: 650px) {
  .wrapper {
    margin: 0 auto;
    max-width: 600px;
    padding: 0 10px;
  }

  .controller {
    background-color: #ffebee;
    border-radius: 4px;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    margin-top: 20px;
    padding: 15px;

    & > .button {
      margin-left: auto;
      margin-right: auto;
      display: block;
      margin-top: 10px;
    }
  }

  .controllerAddGroup {
    display: flex;
    width: 100%;
    justify-content: space-between;
  }

  .controllerInput {
    border: none;
    padding: 10px;
    background-color: #ffcdd2;
    transition: all 0.5s ease;
    width: 100%;

    &:focus {
      background-color: #ffffff;
      outline: none;
    }
  }

  .button {
    appearance: none;
    background-color: #ec407a;
    border: none;
    color: #ffffff;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.5rem;
    margin: 0;
    outline: none;
    padding: 10px;
    transition: all 0.5s ease;
    width: 100px;

    &:hover {
      background-color: #ad1457;
    }

    &:disabled {
      opacity: 0;
      cursor: default;
    }
  }

  .cardGroup {
    display: flex;
    justify-content: space-between;
    margin-top: 20px;
    max-width: 1000px;
  }

  .cardInner {
    width: 100%;
  }

  .cardShape {
    background-color: #e3f2fd;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    padding: 15px;
    max-width: 600px;

    & + .cardShape {
      margin-top: 10px;
    }
  }

  .cardWord {
    color: #212121;
    display: inline;
    font-size: 1rem;
    font-weight: bold;
    line-height: 1.2;
    overflow: hidden;
    padding: 0 20px;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    width: 400px;
  }

  .cardTransition {
    background-color: #1e88e5;
    color: #ffffff;
    border-radius: 4px;
    padding: 5px;
  }
}

src\App.js
import React from "react";

import TransitionList from "./components/transitionList/TransitionList";

function App() {
  return (
    <div className="App">
     <TransitionList />
    </div>
  );
}

export default App;

リポジトリ

https://github.com/takeshisakuma/rtg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを学ぶⅢ(create-react-appの環境構築)

■ はじめに

Reactについて3回目の記事にしました。
この記事で得る内容は以下の通りです。

・create-react-appの環境構築

■ create-react-appに必要なもの

・node 8.10以上
・npm 5.6以上
→インストールの為にhomebrewとnodebrewをインストール(homebrewを使ってnodebrewをインストール)

■ homebrewをインストール・確認

Homebrew公式のインストールをコピー&ペーストして実行する

Terminal
// ① homebrewをインストールする

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
Terminal
// ② homebrewがインストールされているか確認する

brew -v

■ nodebrewをインストール・確認

Terminal
// ③ nodebrewをインストールする

brew install nodebrew
Terminal
// ④ nodebrewがインストールされているか確認する

nodebrew -v

■ nodeをインストール・確認

Terminal
// ⑤ インストール可能なnodeのバージョンを確認する

nodebrew ls-remote
Terminal
// ⑥ 安定版(stable)のnodeをインストールする

nodebrew install-binary stable

No such file or directoryのエラーが表示された場合はこちら

Terminal
// ⑦ インストールしたnodeの確認

nodebrew ls

初めてnodeをインストールすると、currentがnoneになっているので

Terminal
hoge@hogenoMacBook-Air ~ % nodebrew ls
v14.14.0

current: none
hoge@hogenoMacBook-Air ~ %
Terminal
// ⑧ 先程インストールしたnodeのバージョンを指定して、今回は`nodebrew use v14.14.0`と実行する

hoge@hogenoMacBook-Air ~ % nodebrew use v14.14.0
Terminal
// ⑨ 再度`nodebrew ls`を実行し、currentがuseで指定したバージョンになっている事を確認する

hoge@hogenoMacBook-Air ~ % nodebrew ls
v14.14.0

current: v14.14.0
hoge@hogenoMacBook-Air ~ %
Terminal
// ⑩ nodebrewを使える様にパスを通す
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile
Terminal
// ⑪ ターミナルを再起動し、`node -v'と実行し、v14.14.0の様にバージョンが表示されていたら成功
node -v
Terminal
// ⑫nodeをインストールすると、npmもインストールされている
npm -v

お疲れ様でした。次回に続きます・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

REACTチュートリアルメモ

REACTのチュートリアルについてのメモです

1. 開発環境を作る

公式チュートリアルに沿って進めていきます
https://ja.reactjs.org/tutorial/tutorial.html

ブラウザ上で開発するかローカルで開発するか

ブラウザ上で開発出来るスターターコードを使えば開発環境の整備は不要ですが、今回僕はローカルでやりました。

ローカル環境を作る

以下の手順でローカルのプロジェクトを作成してサーバーを起動します

1. Nodejsをインストールする

公式サイトからダウンロードしてインストールします
https://nodejs.org/ja/

2. Create React Appでプロジェクトフォルダを作る

適当なディレクトリで以下を実行します

terminal
npx create-react-app my-app
3. サーバーを起動する

上記を実行するとmy-appというディレクトリが作成されて、デフォルトで必要なファイルが生成されていますので、my-appに移動してnpm startすればサーバーが待機します。

terminal
cd my-app
npm start

http://localhost:3000/ を開くと以下のように表示される筈です

チュートリアルでは作成されたファイルを使用しないので/src/の中のファイルを全部消して作り直します

LinuxまたはMacの場合
cd my-app
cd src
rm -f *
cd ..
Windowsの場合
cd my-app
cd src
del *
cd ..

2. Gameクラスを作成して表示する

チュートリアルでは以下のような3目並べのゲームボードを作成します
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
image.png
これを実現するため、1マスに該当するSquareクラスを9個並べてBoardクラスにして、これをGameクラスのプロパティとしてもたせた上で、ReactDOM.render()に表示してもらいます。

/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
/src/index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

これで先程と同様にnpm startして http://localhost:3000/ にアクセスすると、以下のように表示される筈です。
image.png

3. クラスに値を持たせる/値を参照する

以下のようにしてやればSquareクラスに文字を表示できます

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {"a"}
      </button>
    );
  }
}

classに持たせた値を参照して表示する場合には以下のようにします。Square.render()内でthis.props.valueを参照しているので、Squareクラスのprops(プロパティ)のvalueを参照していることになります。

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

BoardクラスからSquareクラスを作る際に引数としてvalueプロパティを作るようにします。

index.js
class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }
}

Boardクラスの引数通りに0~8の番号を表示するようになりました
image.png

4. インタラクティブなコンポーネントを作る

buttonにonClick={}を追加することでSquareクラスのオブジェクトがクリックされたときにalertを出すようにしてみましょう。以下のように関数を渡してやることでクリックされたときにreactがalert()を出してくれます。

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { alert('click'); }}>
        {this.props.value}
      </button>
    );
  }
}

なお、上記のようにfunctionが重なるとthisがどれを指しているのか分かりにくくなるので下のようにarrow関数で書く方が良いそうです。

index.js
class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => alert('click')}>
       {this.props.value}
     </button>
   );
 }
}

5. クリックしたらXを表示するようにする

まず、コンストラクタ(クラスが最初に呼ばれた時に実行される関数)を追加して状態を記憶させるthis.stateを初期化するようにします。最初にsuper(props)としてるのは、ES2015(ES6)のJavaScriptクラスではsuper()クラスを呼ぶまでthisもsuperも使えなくなる仕様になっているからだそうで、コンストラクタを書くときは常に最初にsuper(props)するように推奨されています。

続いて、buttonに表示される値をthis.state.valueに変更し、onClickでthis.state.valueを'X'に変更するようにします。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }
}

クリックするとコマにXが表示されるようになりました
image.png

6. React DevTools

ChromeまたはFirefoxの拡張機能として提供されているReact DevToolsを使うとReactアプリケーションのpropsとstateを確認できるようになります。

インストール手順は以下を参照してください
https://ja.reactjs.org/docs/optimizing-performance.html

拡張機能を有効にすると、ブラウザの開発者向けツールにreactのcomponentsタグとProfilerタグが追加されて、各クラスのプロパティが確認できるようになります。
image.png

7. 子コンポーネントの値を親コンポーネントに監理させる

ここまでのコードではSquareクラスが自分で表示するための値を保持していましたが、親クラスに監理させた方がコードが分かりやすく、より壊れにくく、リファクタリングしやすくなるそうですので、そのように書き換えていきます。

まず、親クラスにコンストラクタ関数を書いてBoard.state.squaresを初期化、この値を使ってSquareオブジェクトを表示してやります。また、先程までSquareクラスに書いてあったonClickで呼び出す関数もBoardクラスにhandleClick()として書いてやります。これにより値の監理をやりやすくなります。

また、handleClick()内でthis.state.squaresの値を一旦slice()してから書き換えているのはコードをイミュータブルにする為です。元の値を直接いじらないイミュータブルな書き方にする事で変更の有無を検出しやすくしたり、変更の履歴を保存したり、render()すべきタイミングを把握しやすくする効果が期待できるそうです。

index.js
class Board extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]} 
        onClick={() => this.handleClick(i)} 
      />
    );
  }

Squareでやっていた値の監理がなくなったのでコンストラクタは消し、onClickでは親クラスから受け取ったSquare.props.onClick()関数(中身はBoard.handleClick())をonClickで実行するように書いてやります。

index.js
class Square extends React.Component {
  render() {
    return (
      <button 
        className="square" 
        onClick={() => this.props.onClick()}
      >
        {this.props.value}
      </button>
    );
  }
}

クリックするとBoardクラスにstateの値が変わり、それに合わせてXが表示されていくようになりました。
image.png

これによりSquaredはクリックされたことを親クラスに伝えるだけのコンポーネントになって、ロジックが書きやすくなりました。

8. 関数コンポーネント

上記の変更によりSquareクラスに値を持たなくなったのでクラスでなく関数で書いたほうが簡潔になります。{() => this.props.onClick()}の代わりにprops.onClickと書き換えるので、以下のようにかなり短く書くことができます。

index.js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
        {props.value}
    </button>
  );
}

9. 手番の処理

Xの手番の次はOにならないといけないので、次の手番がXOのどちらなのか書いてやります。BoardクラスにxIsNextプロパティを設定して、trueだったら次はXの手番、falseだったら次はOの手番として処理します。

index.js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
        {props.value}
    </button>
  );
}

class Board extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]} 
        onClick={() => this.handleClick(i)} 
      />
    );
  }

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

image.png

10. 勝者を判定する

勝者を判定する関数を作成します。squaresは初期値がnullなのでif文からするとfalseと扱われるのを利用してnullではない値が何れかのlineすべてに入っていたら勝者は入っていた値であるという関数ですね。

index.js
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

Board.render()内でcalculateWinner()を使って勝者判定を行い、勝者を表示するようにします。

render() {
  const winner = calculateWinner(this.state.squares);
  let status;
  if (winner){
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
  }

また、勝者が決まった後に手番が進められるとおかしいので、Board.handleClick()内でcalculateWinner()がnullでないときは手番の更新を行わないようにします。

handleClick(i) {
  const squares = this.state.squares.slice();
  if (calculateWinner(squares) || squares[i]) {
    return;
  }
  squares[i] = this.state.xIsNext ? 'X' : 'O';
  this.setState({
    squares: squares,
    xIsNext: !this.state.xIsNext,
  });
}

image.png

11. 履歴の保存

履歴を残しておいて手番を戻ることができるようにします。具体的には、以下のようなフォーマットで履歴を保存しておいて、戻れるようにしてやります。

こんな風に保存したい
history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

上記のhistoryをGame.props.statesとして管理するように書き換えます。

handleClick()でhistoryに最新のsquaresを追加して上書きすることで1つずつ追加される挙動を作っています。

チュートリアルではこれに並行してBoardクラスで管理してた値をまとめてGameクラスで管理するように変更する作業もやってますので、以下のようにごっそり書き換えてしまってますが、自分でやるときはまず値の管理を置き換えて、正常に動いてから機能追加した方が良いと思います。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
        {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square 
        value={this.props.squares[i]} 
        onClick={() => this.props.onClick(i)} 
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    }
  }

  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner){
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board 
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

これで、Gameクラス内に履歴が残るようになりました。
image.png

12. 履歴の表示

とりあえず表示

ここでJavaScriptの標準機能である配列のmapメソッドを使います

mapメソッドの動作例
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

Game.render()内でhistoryの値からbutton表示させるhtmlを生成し、olタグ内に貼り付けることで戻るためのbuttonを作ります。

index.js
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      )
    })

    let status;
    if (winner){
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board 
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }

この時点ではbuttonタグのonClick()で呼ばれるjumpTo()を書いていないのでbuttonを押すとエラー停止になります。
image.png

historyにkeyを追加

この時点で実行するとconsoleに以下のような警告がでます
image.png
リスト項目に固有のkeyが置かれていないので良くないですよ、と警告されています。順番だけで管理してると項目が増えた時に混乱するのでチュートリアルにも「動的なリストを構築する場合は正しい key を割り当てることが強く推奨されます」とありますから、素直に従ってkeyを追加してやります。

具体的には

{hoge(move)}を{hoge(move)}と書き換えます。reactがkey={move}を認識してそこに書かれたhoge(move)にGame.state.moveを渡せば良いんだと理解してくれます。
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      )
    })

警告が出なくなってくれました
image.png

jumpTo()を書く

戻るための関数を書いていきます。まずconstructorに現在表示している手番が何番目かを示すstepNumberを設定し、jumpTo()するとstepNumberとxIsNextをその時点での値に戻し、その後render()する際に表示する盤面を最新のものhistory[history.length - 1]ではなく指定されたものhistory[stepNumber]に変更、handleClick()されたら手戻りしたところまでのhistoryに追加して書き加えていくようにします。

jumpTo()の時点でhistoryを書き換えてしまうと履歴をウロウロすることが出来なくなっちゃいますので新たに手番が指されたときに更新するようにしてるわけですね。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    }
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      )
    })

ということで完成したコードがこちらです
https://codepen.io/gaearon/pen/gWWZgR?editors=0010

13. buildする

アプリケーションが完成しましたのでデプロイ用にbuildしましょう。

まず、相対パスで書けるようにpackage.jsonに"homepage": "./"を追記します。

package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "homepage": "./",

あとはプロジェクトフォルダでnpm run buildするだけです

terminal
cd my-app
npm run build

プロジェクトフォルダ内にbuildフォルダが生成されています
image.png
このindex.htmlをブラウザで開くとアプリケーションが表示されます
image.png

14. 次に何をやれば良いか

この先、ドキュメントはreact.jsの主なコンセプトの解説へと続いていきます。JSXやstateのライフサイクル、イベント処理やReactの流儀など重要な項目にフォーカスして説明しているので続いて読んでいくのが良さそうです。
https://ja.reactjs.org/docs/hello-world.html

  1. Hello World
  2. JSX の導入
  3. 要素のレンダー
  4. コンポーネントと props
  5. state とライフサイクル
  6. イベント処理
  7. 条件付きレンダー
  8. リストと key
  9. フォーム
  10. state のリフトアップ
  11. コンポジション vs 継承
  12. React の流儀

感想

チュートリアルをやる前にREACTは学習コストが高くて云々という記事も多くみかけたんですが、classベースで書くやり方も情報を上位クラスで保持する書き方もそこそこ複雑なコードを書く時の作業性に効いてきそうですし、デバッグがやりやすいようにブラウザ拡張ツールでエラーコードが確認できるのもありがたいです。少なくとも、生のJavaScriptよりは大幅にコードが書きやすいのでreact.jsを食わず嫌いしてるウェブ開発ビギナーはチュートリアルだけでもやってみてほしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

memo? useCallback? パフォーマンスが気になる JSXer には SolidJS がオススメ

パフォーマンスなんか気にしたくない

Give a man a bug and he'll work for a day.
Give a man a benchmark and he'll work for a lifetime.1
https://twitter.com/awesomekling/status/1318615899910307850

パフォーマンスなんかに気をとられながら実装したくないんですよ。
Reactmemo()useCallback() のような最適化のためだけの API を呼ぶ呼ばないで 1 ミリ秒も悩みたくないんです。そんな API は存在しないでほしい。

でも気になっちゃうんです。というか、まったく最適化せずに React でアプリを構築していくと、カクつきを体感するぐらいには遅くなりますよね。気にせざるを得ません。

つい最近も React のパフォーマンスチューニング記事がバズってましたね。

早すぎる最適化は害悪か

早すぎる最適化は諸悪の根源とまで言われています。とても強烈な言葉です。本当にそうでしょうか? React でもそうでしょうか?

memo() をガチで導入しようと思ったら、 MobX のようなリアクティブなグローバルデータストアを各コンポーネントから直接参照するか、あるいは状態ツリーを不変にするか(そのために Immutable.jsImmer を導入しようか)、みたいな話になるかと思います2実装が進んでから最適化しようと思ったら、状態の持ち方から変えることになるかもしれません。これはさすがにつらい。

パフォーマンス問題は得てして発見が遅れがちです。普段の開発用 PC ではサクサク動くのに、 2 年前に出たローエンドスマホでテストしてみたら全然動かない、みたいな形で姿を現します。手元では再現しません。ローエンドスマホでのデバッグで遅い原因を突き止めて最適化するのはなかなかの試練です。

そして 1 度でもパフォーマンスが課題になるとソースコードのあっちもこっちも気になって気になってムズムズしてきます。最適化はどれだけ時間があっても足りません。(ムズムズしても、極端に遅い箇所だけの最適化にとどめて他の新機能開発に時間を使うのがオトナというもの。最適化に時間を費やしすぎることこそが害悪なのです。...なかなかオトナにはなれないものですけどね...)

最初からなるべくパフォーマンスが問題にならないような仕組みを導入しておくのが無難なのです。
早すぎる最適化はリスク回避です。 少なくともこの文脈では。

可能なら意識しなくても十分にパフォーマンスが出るようなライブラリを選びたいところですよね。

ということで、 React に似た API と JSX で、勝手に爆速になる UI ライブラリ Solid を紹介します。

Solid

Solid is a declarative JavaScript library for creating user interfaces. It does not use a Virtual DOM. Instead it opts to compile its templates down to real DOM nodes and wrap updates in fine grained reactions. This way when your state updates only the code that depends on it runs.

[DeepL 翻訳]
Solid は、ユーザー インターフェイスを作成するための宣言型 JavaScript ライブラリです。仮想 DOM は使用しません。その代わりに、テンプレートを実際の DOM ノードまでコンパイルして、更新をきめ細かいリアクションでラップします。これにより、状態が更新されたときに、それに依存するコードのみが実行されます。

Solid は、
React のように JSX とフックライクな API で宣言的な関数コンポーネントを記述でき、
lit-html のように効率的にテンプレートから DOM を構築・更新し、
Svelte のようにコンパイル時に再レンダリングを最適化し、
SSR もサポートする、軽量で非常に高速な UI ライブラリです。

公式コード例(TSX)

import { createState, onCleanup, Component } from "solid-js";
import { render } from "solid-js/dom";

const App: Component = () => {
  const [state, setState] = createState({ count: 0 }),
    timer = setInterval(() => setState("count", c => c + 1), 1000);
  onCleanup(() => clearInterval(timer));

  return <div>{state.count}</div>;
};

render(() => <App />, document.getElementById("app"));

https://codesandbox.io/s/8no2n9k94l?file=/index.tsx

なんとなく読めるのではないでしょうか。

React と特に大きく異なるのは、関数コンポーネント内の処理が要素初期化時に 1 度だけ呼ばる点です。 React のようにレンダリングのたびに呼ばれるわけではありません。ここは挙動の違いを明確に意識する必要があると思います。

React の memo()useCallback() のような最適化用の API はありません。それでいて React や Vue.js (3.0) はもちろん、 lit-html や Svelte よりも高速に動作します。ベンチマークを見ると vanillajs にかなり近いです。

js-framework-benchmark.png

TSX で書けて型チェックできるし、 API もわりと扱いやすいし、この実行速度。

(私の観測範囲では) 控えめに言って最強です (2020 年 10 月現在)。

注意点

どんなライブラリにもクセはあります。私は React の onChange"input" イベントで発火するのが許せない人です。私は Mr. Complain です3Angular のこともいろいろ書きました。 まだぜんぜん使い込んでいませんが Solid に対しても不満はあります。

state や props をデストラクチャリングしちゃダメ

createState() で作成する状態やコンポーネントの引数 props はフィールドアクセスが変更監視のトリガーになっていて、リアクティブにしたい式の中でアクセスする必要があります。デストラクチャリングすると、プロパティの変更に対してリアクティブに動作しなくなってしまいます。

変更監視系 API をコンポーネントの外で使うと警告

Solid では createSignal() API によってリアクティブでアトミックな状態、 createState() API によってリアクティブな状態ツリーを作ります。これらは React の useState() と異なり、グローバルスコープで普通に作ればコンポーネント間で状態を共有できます。(フックと違ってコンポーネント内で呼ぶ順番も関係ありません。)非常に便利です。

これら状態を監視して変更に反応する、たとえば createMemo() (MobX でいう computed()、 Vue.js でいう computed()、 Recoil でいう selector())などの API もグローバルスコープで期待通りに動作します。が、警告が出ます。

computations created outside a createRoot or render will never be disposed

この警告の意図は理解できます。が、現実のユースケースに照らして考えると、この警告は親切すぎるというか、大きなお世話だと感じます。
Solid の作者によれば、グローバルな状態には Context API がオススメとのことですが、めちゃくちゃ単純で直感的な createSignal() + createMemo() に比べると Context API は複雑で扱いづらいです。
グローバルな状態を作る方法として、変更監視 API の実際の呼び出しをルートコンポーネントの初期化まで遅延させる手は使えそうです。
https://codesandbox.io/s/lmrb9?file=/index.tsx

ためしに作ってみたもの

ライフゲームの例では React + Recoil、 Preact + preact-shared-state-hook、 Vue.js 3.0、 Solid でライフゲームロジックを共有して FPS を比較してみました。 Solid はバンドルサイズが最小になり、かつ、 FPS も他と比べると群を抜いて良い結果になりました(ベンチマークに適した例ではないかもしれませんが)。(参考に手元の PC で React 12 fps、 Preact 12 fps、 Vue.js 8 fps、 Solid 28 fps 程度です。)

おわりに

Solid は React に近い API と Vanilla JS に近い実行速度を持つ優れた UI ライブラリです。パフォーマンスのことばかりに気をとられる日々は終わりを迎えるかもしれません。

とはいえ、まだ歴史の浅いライブラリですのでエコシステムが未成熟です。コンポーネントフレームワーク(Material-UI とか Element UI みたいなの)は見当たらないし、ネット上で見つかる事例も少ないです。日本語の記事ぜんぜんないし。いま使うなら多少の苦労は覚悟しなきゃいけないかもしれません。

私はその苦労の価値があるんじゃないかと踏み、 Solid に期待を込めて、この記事を書いている次第です。

参考


  1. 「魚を与えればその人は一日の糧を得、魚の釣り方を教えればその人は一生の糧を得る」という老子の言葉のオシャレなパロディですね。 

  2. Redux を導入しようか、という話にはまずならないと私は思います。 

  3. Yeah my life is shit 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モダンな技術のReactを勉強しよう

最近現役エンジニアの方とお話しさせていただく機会があったのですが、やっぱり現場はReact一択!!とういう声が大きかったので、どんな技術なのか、どうやって導入するのか調べて実際に簡単なアプリを作成していこうと思うので、これからReactをアウトプットして行こうと思います。

Reactとは

まずは簡単にReactってどういうものなのかざっと調べた情報を記述します

  • ReactはFacebookが作った
  • JSのライプラリ
  • 記述の仕方はJSX(ほぼHTML)
  • {}内はJSの記述できる
  • SPA(Single Page Application)を作ることに最適
    • ちなみにSPAはページ推移せず、一つのWebページでコンテンツを切り替えられるやつのこと いちいちリロードが必要ないやつ
  • TypeScriptと相性が良い

ほんとに簡単に調べた結果を羅列しただけですが、こんな感じ

モダンな技術を学ぶのはわくわくしますね! オラわくわくすっ(ry
まあよく意味わからないところとかあるんですけど、それはアプリ作りながらやっていけばわかる様になっていくのかなと思います
じゃけん、環境構築やっていきましょうか!(埼玉県出身者より)

環境構築

create-react-appという開発環境を簡単に構築できるツールを使っていくのですが、どうやらそれを使うためには
node
npm
が必要らしいです

またそれをインストールするためにはHomebrewnodebrewをインストールしなければいけない様です、、ややこしい、、

なんでまずはこのコマンド

ターミナル
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 

その後、Macのパスワードと
Press RETURN to continue or any other key to abort
と出てくるのでreturn押してあげてください
これでHomebrewのインストール完了

つぎはこのHomebrewを使ってnodebrewをインストールします

ターミナル
brew install nodebrew

これでnodebrewのインストール完了
そんで次にnodebrewを使ってnodeをインストールします

ターミナル
nodebrew install stable

ディレクトリがないとエラー出る場合はこれを先に

ターミナル
mkdir -p ~/.nodebrew/src

インストールが完了したら

ターミナル
nodebrew ls

でバージョンを確認しましょう
多分今の安定バージョンというのはv14.4.0になっていて、それをインストールした状態ですが
current: none
とターミナルに記載があると思います。
これをv14.4.0にするために

ターミナル
nodebrew use v14.4.0

これで再度バージョンを確認すると
current: v14.4.0
になっているはず

そして次はこれ(最新macOSなら)

ターミナル
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile

これでやっとnodeがインストールできました!!
nodeがインストールされたらnpmもインストールされていますので、これでcreate-react-appを使う環境が整ったとういう感じですね!

そしてこれが最後のアプリケーションを作成するときにコマンド

ターミナル
npx create-react-app 任意のアプリケーション名

これでアプリケーションが作成されました!!
Finderから確認できればアプリケーションは作成されています
長かった?ですね

あっ、アプリケーションの作成の際はちゃんと自分が作りたいディレクトリに移動して作成してくださいね!

まとめ

とりあえずcreate-react-appの環境構築までをずらずら記述していったのですが、ネットワークの基礎的な部分が圧倒的に不足していますね。。。
なんでかわからないけどできたという状態から抜け出すためにはどのくらい勉強すればいいのかトホホ、、、

参考

https://www.youtube.com/watch?v=P5lDbRxp9sI&t=140s

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebaseでのデプロイ時のエラー(2020年10月27日現在)

はじめに

Firebaseにてデプロイを行った際に起きたエラーについて時系列でまとめていきます。

ちなみに、トラハックさんがすごく丁寧に解説しているので参考にすると良いかもしれません。(最近Firebaseのルールが変わったためにエラーが起きているみたいです。)
日本一わかりやすいReact入門【実践編】#3...Firebaseプロジェクトの作成と初めてのデプロイ

Error: functions predeploy error: Command terminated with non-zero exit

package.jsonの「"react-app"」の部分だけ削除します。

package.json
  "eslintConfig": {
    "extends": [
      "react-app"  ←ここだけ
      "react-app/jest"
    ]
  },

これで通ります。

 Error: HTTP Error: 400, Billing account for project '1050404670100' is not found. Billing must be enabled for activation of service(s) 'cloudbuild.googleapis.com,containerregistry.googleapis.com' to proceed.

何やら長い文。
簡単に訳すと、

「請求先がないんだけど!!デプロイしたいなら有料プランにしてな!!」

という事らしいです。

(ええ。個人のテスト開発だし、どうせなら無料のままが良いんだが。)と悩んでいても意味ないです。Google様にこう言われたら従うしかないのです。。

ただ、有料プラン(Blaze)に変更しても無料枠が大きいので、個人開発レベルでは課金されることはほぼないようです。また、課金されそうになると通知を送ってくれる機能もあるのでそこまで心配しなくても良いかと思います。

ということで有料プランに変更。

変更の方法については、申し訳ありません、省略させていただきます。??‍♂️

ほんでもって、firebase deployをすると、、、

✔  Deploy complete!

やったーーーーー!できた!

さいごに

ご覧いただきありがとうございました。お役に立てていれば幸いです。
(ご指摘等あればバンバンお願いいたします!)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

reactでJSONデータを扱うときはJSON.perseを利用する

// JSONを定義
const json = {"domain":"vamdemic", "count":1};

// 一度JSON形式へ変換
const convert_json = JSON.stringify(json);

// 変換したものを読み込む
const obj = JSON.parse(convert_json);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む