公開日:2021.12.24

モダンフロント学習街道~React編~

テクログ

こんにちは!前回に引き続きモダンフロント学習やっていきます!今回はReactです!

教材はUdemyの下記講座です!

モダンJavaScriptの基礎から始める挫折しないためのReact入門https://www.udemy.com/course/modern_javascipt_react_beginner/

JSX記法のルール

// Reactコンポーネントを書く場合、お作法としてreactのインポートが必要(React17のアップデートでJSXを使用するだけの場合は記述不要になった)
import React from "react";
// htmlにコンポーネントを反映するために、react-domをインポートする
import ReactDom from "react-dom";

// Reactでは関数を使用して画面の要素であるコンポーネントを表現していけるので、アロー関数を書いていく
const App = () => {
  return (
    <> 
      <h1>こんにちは!</h1>
      <p>お元気ですか?</p>
    </>
  );
};

// Appをコンポーネントとしてレンダリングする
// 第一引数にレンダリングしたいコンポーネント、第二引数に反映箇所を指定する
ReactDom.render(<App />, document.getElementById("root"));

まとめ

jsxはjavascriptの中にHTMLを書いていく。
ファイル拡張子はjsxとする。
returnの内容が複数になる場合は()で囲う必要がある。
returnしていく内容は1つのタグで囲わないとエラーになるので、フラグメント(<>)で囲ってあげる。

コンポーネント

コンポーネント化

import React from "react";
import ReactDom from "react-dom";

const App = () => {
  return (
    <> 
      <h1>こんにちは!</h1>
      <p>お元気ですか?</p>
    </>
  );
};

// コンポーネントをエクスポートして、他のファイルでインポートできるようにする。
export default App;

まとめ

コンポーネントとは画面要素の1単位。大(1画面)から小(1つのテキストボックス)まで様々。
コンポーネント名は必ず先頭を大文字から始めるようにする。
コンポーネントをエクスポートして、他のファイルでインポートできるようにする。

イベントの扱い方

import React from "react";
import ReactDom from "react-dom";

const App = () => {
  // ボタンクリックで呼び出すアロー関数を定義する
  const onClickButton = () => alert();

  return (
    <> 
      <h1>こんにちは!</h1>
      <p>お元気ですか?</p>
      <button onClick={onClickButton}>ボタン</button>
    </>
  );
};


export default App;

まとめ

HTMLタグの中で扱うイベントやスタイルの名称はキャメルケースとする。
{}で囲った部分はjavascriptを書くことができる。

Props

import React from "react";

// ColorfulMessageコンポーネントを定義する
// 引数としてpropsを受け取る
export const ColorfulMessage = (props) => {
  // propsオブジェクトから分割代入でcolorとchildrenを受け取る
  const { color, children } = props;
  const contentStyle = {
    color,
    fontSize: "18px"
  };
  return <p style={contentStyle}>{children}</p>;
};

import React from "react";
// ColorfulMessageコンポーネントをインポートする
import { ColorfulMessage } from "./components/ColorfulMessage";

const App = () => {
  // ボタンクリックで呼び出すアロー関数を定義する
  const onClickButton = () => alert();

  return (
    <> 
      <h1>こんにちは!</h1>
      <!-- ColorfulMessageコンポーネントにcolorというPropsを渡す -->
      <ColorfulMessage color="blue">お元気ですか?</ColorfulMessage>
      <ColorfulMessage color="pink">元気です!</ColorfulMessage>
      <button onClick={onClickButton}>ボタン</button>
    </>
  );
};


export default App;

まとめ

Propsとはコンポーネントに対して渡す引数のようなもの。
コンポーネントタグで囲んだ要素はpropsオブジェクトのchildrenプロパティとして受け取ることができる。

State(useState)

// useStateをインポート
import React, { useState } from "react";
import { ColorfulMessage } from "./components/ColorfulMessage";

const App = () => {
  // ボタンが押されたら、State(num)をインクリメントして更新する
  const onClickCountUp = () => {
    setNum(num + 1); // UseState関数から分割代入で受け取ったState更新用の関数を呼び出す
  };
  // Stateを扱う場合、useStateという関数を使用する
  // useStateの中から配列の分割代入で1つ目として「Stateとして使用する変数名」2つ目として「Stateの更新用の関数」を受け取る
  const [num, setNum] = useState(0);   // [変数名, set変数名]と命名するのが一般的

  return (
    <> 
      <h1>こんにちは!</h1>

      <ColorfulMessage color="blue">お元気ですか?</ColorfulMessage>
      <ColorfulMessage color="pink">元気です!</ColorfulMessage>
      <!-- onClickイベントにState更新処理を登録 -->
      <button onClick={onClickCountUp}>カウントアップ!</button>
      <!-- State(num)を表示する onClickCountUpが呼ばれるたびにインクリメントされた状態で表示が更新される -->
      <p>{num}</p>
    </>
  );
};

export default App;

まとめ

Stateとはそれぞれのコンポーネントが持っている可変の状態のこと。
条件によって動的に変わる部分をStateとして定義してあげることで様々な画面を表示していくことができる。
コンポーネント内で動的に変わる部分は、useStateで定義して変数と更新関数と初期値を設定していく。
Stateが変更されると再レンダリングする。

再レンダリングと副作用(useEffect)

// useEffectをインポートする
import React, { useEffect, useState } from "react";
import { ColorfulMessage } from "./components/ColorfulMessage";

const App = () => {
  const [num, setNum] = useState(0);
  const [faceShowFlag, setFaceShowFlag] = useState(false);

  const onClickCountUp = () => {
    setNum(num + 1);
  };
  const onClickSwitchShowFlag = () => {
    setFaceShowFlag(!faceShowFlag);
  };

  // 
  /** useEffectは第一引数に関数、第二引数に依存配列を取る
   *  第二引数に空配列を渡すと、そのコンポーネントで最初の1回だけ通したいような処理を実行することができる。
   *  例:画面初回表示時にデータを取得したり、処理を実行する時
   *  第二引数の配列に変数を渡すと、その変数だけに関心を持つuseEffectになる
   *  例:numの更新が走る度に以下のuseEffect内の関数が実行される
   */ 
  useEffect(() => {
    if (num > 0) {
      if (num % 3 === 0) {
        faceShowFlag || setFaceShowFlag(true);
      } else {
        faceShowFlag && setFaceShowFlag(false);
      }
    }
    // es-lintを使用して静的解析を行っている場合、依存配列には関数内で使用している変数をすべて入れないと警告が出る。特定の変数のみに関心を持ちたい場合は、下記のように「コメント」で設定を無視する記述を書くことで次の行の警告が出なくなる
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [num]); 

  return (
    <> 
      <h1>こんにちは!</h1>

      <ColorfulMessage color="blue">お元気ですか?</ColorfulMessage>
      <ColorfulMessage color="pink">元気です!</ColorfulMessage>

      <button onClick={onClickCountUp}>カウントアップ!</button>
      <br />
      <!-- on/offボタンが押される度にonClickSwitchShowFlagを切り替える -->
      <button onClick={onClickSwitchShowFlag}>on/off</button> 
      <p>{num}</p>
      {faceShowFlag && <p>(´・ω・`)</p>}
    </>
  );
};

export default App;

まとめ

Reactの再レンダリング条件は「Stateを変更した時」「Propsを受け取っている場合、そのPropsの中身に変更があった時」「親のコンポーネントが再レンダリングされる時、子コンポーネントも追従して再レンダリング」の3つとなる。
再レンダリングを最適化してパフォーマンスを上げる必要がある。
「Too many re-renders」のエラーが出た場合は、Stateの更新周りを疑う。
扱う変数やStateの数が増えてくると、それぞれの処理が邪魔しあってうまく動かなくなる場合がある。
そのような場合はuseEffectを使用して関心の分離を行う。
ある変数の状態に応じてや、初回レンダリング時のみに処理をしていきたい時にuseEffectは非常に使える。

default exportとexport

default exportの場合

import React from "react";

const ColorfulMessage = (props) => {
  const { color, children } = props;
  const contentStyle = {
    color,
    fontSize: "18px"
  };
  return <p style={contentStyle}>{children}</p>;
};

export default ColorfulMessage;

通常のexportの場合

import React from "react";

export const ColorfulMessage = (props) => {
  const { color, children } = props;
  const contentStyle = {
    color,
    fontSize: "18px"
  };
  return <p style={contentStyle}>{children}</p>;
};

まとめ

exportの仕方は「関数名を定義して、その下で【export default 関数名】とするやり方」と「関数定義の先頭にexportをつけるやり方」の2つがある。
Reactに関してはコンポーネントの名称が確約される(分割代入して定義元の関数名をコンポーネント名としてインポートしなければならない)ので、通常のexportにする方が恩恵がある(コンポーネント名のタイポの心配がない)。

レンダリングの最適化(memo/useCallBack/useMemo)

memo

import { memo } from "react";

const style = {
  width: "100%",
  height: "200px",
  backgroundColor: "khaki"
};
// memo()でコンポーネント自体(アロー関数)を囲うことで、propsが変更されない限り再レンダリングさせないことができる
export const ChildArea = memo((props) => {
  const { open, onClickClose } = props;
  const data = [...Array(2000).keys()];
  // 2000件の配列をループで回してコンソールログを出力する(ChildAreaコンポーネントのレンダリングの度に走る重い処理)
  data.forEach(() => {
    console.log("...");
  });
  console.log("ChildAreaがレンダリングされた!");

  return (
    <>
      {open ? (
        <div style={style}>
          <p>子コンポーネント</p>
          <button onClick={onClickClose}>閉じる</button>
        </div>
      ) : null}
    </>
  );
});

useCallBack

import { useState, useCallback, useMemo } from "react";

import { ChildArea } from "../ChildArea";
import "./styles.css";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");
  const [open, setOpen] = useState(false);
  const onChangeText = (e) => setText(e.target.value);
  const onClickOpen = () => setOpen(!open);

  /** 関数の処理が変わらない場合は、同じものを使い回させたいのでuseCallBackで囲ってあげる
   *  useCallBackは第一引数に関数、第二引数に依存配列を受け取る
   *  第二引数に空配列を指定すると、初回レンダリングに生成された関数を使い回す
   *  第二引数に変数を指定すると、その変数に変更があった場合のみ関数を再生成する
   */
  const onClickClose = useCallback(() => setOpen(false), [setOpen]);
  const temp = useMemo(() => 1 + 3, []);
  console.log(temp);

  return (
    <div className="App">
      <input value={text} onChange={onChangeText} />
      <br />
      <br />
      <button onClick={onClickOpen}>表示</button>
      // <子コンポーネントのPropsにアロー関数を渡す時、子コンポーネント側で毎回新しい関数を生成していると判断される(関数を普通に渡すだけだと、子コンポーネントをmemo化していてもPropsが変更されたと判断されるので子コンポーネントの再レンダリングが走ってしまう)
      <ChildArea open={open} onClickClose={onClickClose} />
    </div>
  );
}

useMemo

import { useState, useCallback, useMemo } from "react";
import { ChildArea } from "../ChildArea";

import "./styles.css";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");
  const [open, setOpen] = useState(false);
  const onChangeText = (e) => setText(e.target.value);
  const onClickOpen = () => setOpen(!open);
  const onClickClose = useCallback(() => setOpen(false), [setOpen]);
  /** 変数自体のメモ化をuseMemoで行うことができる
   *  useMemoは第一引数に関数、第二引数に依存配列を受け取る
   *  第二引数に空配列を指定すると、初回レンダリングに生成された変数を使い回す
   *  第二引数に変数を指定すると、その変数に変更があった場合のみ変数を再生成する
   */
  const temp = useMemo(() => 1 + 3, []);
  console.log(temp);

  return (
    <div className="App">
      <input value={text} onChange={onChangeText} />
      <br />
      <br />
      <button onClick={onClickOpen}>表示</button>
      <ChildArea open={open} onClickClose={onClickClose} />
    </div>
  );
}

まとめ

基本的に、複数の要素から成り立っているコンポーネントや、今後肥大化が予想されるコンポーネントはmemo()で囲ってあげる。
関数の処理が変わらない場合は、同じものを使い回させたいので関数全体をuseCallBackで囲ってあげる。
Propsに関数を受け取るコンポーネントの再レンダリングを最適化する場合は、コンポーネントのmemo化と関数定義にuseCallBackを設定することをセットとする。
変数自体のメモ化をuseMemoで行うことができる。
そこまで使う機会はないが、変数に設定する中の処理が複雑になっている時などのケースでは有効である。

ルーティング(React Router)

import { BrowserRouter, Link } from "react-router-dom";

import { Router } from "./router/Router";

import "./styles.css";

export default function App() {
  return (

    <!-- BrowserRouterコンポーネントで囲った範囲でルーティングが有効になる -->
    <BrowserRouter>
      <div className="App">
        <!-- Linkコンポーネント(HTMLでいうaタグの役割)を使用することで簡単にページ遷移の処理を行うことができる Propsのtoに遷移したいURLのパスを指定する-->
        <Link to="/">Home</Link>
        <br />
        <Link to="/page1">Page1</Link>
        <br />
        <Link to="/page2">Page2</Link>
      </div>
      <Router />
    </BrowserRouter>
  );
}

import { Switch, Route } from "react-router-dom";

import { Home } from "../Home";
import { Page404 } from "../Page404";
import { Page1Routes } from "./Page1Routes";
import { Page2Routes } from "./Page2Routes";

export const Router = () => {
  return (
    <!-- パス毎に呼び出すコンポーネントを出し分ける場合はSwitchコンポーネントで囲った中でRouteコンポーネントを使用する -->
    <Switch>
      <!-- Propsにexcatを渡すことで、pathに完全一致したものを対象にすることができる -->
      <Route exact path="/">
        <Home />
      </Route>
      <Route
        path="/page1"
        <!--RouteコンポーネントにPropsにrenderを渡すことができる。renderにアロー関数を設定し、その中でコンポーネントを返却するとレンダリングするコンポーネントを指定することができる -->
        <!-- renderはデフォルトでPropsを受け取っている。その中身はルーティングに関する情報を持ったオブジェクトである。プロパティとしてhistory/location/matchのオブジェクトとstaticContextを持っている。例えば、renderオブジェクトの中のmatchオブジェクトのurlプロパティを参照するとURLの相対パスを受け取ることができる -->
        render={({ match: { url } }) => (

          <!-- Switchは入れ子にできる。この場合、子のRouteコンポーネントpathを親のRouteコンポーネントに指定したpathをルートとした記述とすることができる。 -->
          <Switch>
            <!-- 別ファイルに配列として切り出したパス設定を呼び出してループしてルーティングする事もできる -->
            {Page1Routes.map((route) => (
              <Route
                key={route.path}
                exact={route.exact}
                path={`${url}${route.path}`}
              >
                {route.children}
              </Route>
            ))}
          </Switch>
        )}
      />
      <Route
        path="/page2"
        render={({ match: { url } }) => (
          <Switch>
            {Page2Routes.map((route) => (
              <Route
                key={route.path}
                exact={route.exact}
                path={`${url}${route.path}`}
              >
                {route.children}
              </Route>
            ))}
          </Switch>
        )}
      />
      <!-- 最後にRouteのpath="*"を指定することでどれにも一致しなかった場合のルーティングを設定できる -->
      <Route path="*">
        <Page404 />
      </Route>
    </Switch>
  );
};

import { Page1 } from "../Page1";

import { Page1DetailA } from "../Page1DetailA";
import { Page1DetailB } from "../Page1DetailB";

export const Page1Routes = [
  {
    path: "/",
    exact: true,
    children: <Page1 />
  },
  {
    path: "/detailA",
    exact: false,
    children: <Page1DetailA />
  },
  {
    path: "/detailB",
    exact: false,
    children: <Page1DetailB />
  }
];

import { Page2 } from "../Page2";

import { UrlParameter } from "../UrlParameter";

export const Page2Routes = [
  {
    path: "/",
    exact: true,
    children: <Page2 />
  },
  {
    path: "/:id", <!-- :の後にURLパラメータ名を指定できる -->
    exact: false,
    children: <UrlParameter />
  }
];

import { useParams, useLocation } from "react-router-dom";

export const UrlParameter = () => {

  // useParamsというHooksを使用することで、URLパラメーターを受け取ることができる
  const { id } = useParams();

  // useLocationというHooksを使用することで、searchとしてURLクエリパラメーターを受け取ることができる
  const { search } = useLocation();
  // javascript標準のURLSearchParams関数に引数としてsearchを渡すことでURLクエリパラメーターを便利に扱える
  const query = new URLSearchParams(search);

  return (
    <div>
      <h1>UrlParameterページです</h1>
      <p>パラメーターは {id} です</p>
      <p>クエリパラメーターは {query.get("name")} です</p>
    </div>
  );
};

import { Link } from "react-router-dom";

export const Page2 = () => {
  return (
    <div>
      <h1>Page2ページです</h1>
      <Link to="/page2/999">URL Parameter</Link>
      <br />
      <Link to="/page2/999?name=hogehoge">Query Parameter</Link>
    </div>
  );
};

import { Link, useHistory } from "react-router-dom";

export const Page1 = () => {
  const arr = [...Array(100).keys()];
  // useHistoryというHooksを使用することで、javascriptを使用しての画面遷移を簡単に扱うことができる
  const history = useHistory();
  // history.pushを使用することで引数に指定したpathに遷移させることができる
  const onClickDetailA = () => history.push("/page1/detailA");

  return (
    <div>
      <h1>Page1ページです</h1>
      <!-- Linkコンポーネントのtoの中はオブジェクトを渡すこともできる。-->
      <Link to={{ pathname: "/page1/detailA", state: arr }}>DetailA</Link>
      <br />
      <Link to="/page1/detailB">DetailB</Link>
      <br />
      <button onClick={onClickDetailA}>DetailA</button>
    </div>
  );
};

import { useLocation, useHistory } from "react-router-dom";

export const Page1DetailA = () => {
  // useLocationでstateとして任意の変数を受けることもできる
  const { state } = useLocation();
  // useHistoryというHooksを使用することで、javascriptを使用しての画面遷移を簡単に扱うことができる
  const history = useHistory();
  // history.goBackを使用することで前画面に遷移させることができる(ブラウザのバックボタンを同様の挙動)
  const onClickBack = () => history.goBack();
  console.log(state);

  return (
    <div>
      <h1>Page1DetailAページです</h1>
      <button onClick={onClickBack}>戻る</button>
    </div>
  );
};

まとめ

BrowserRouterコンポーネントで囲った範囲でルーティングが有効になる。
パス毎に呼び出すコンポーネントを出し分ける場合はSwitchコンポーネントで囲った中でRouteコンポーネントを使用する。
RouteコンポーネントのPropsにexcatを渡すことで、pathに完全一致したものを対象にすることができる。
RouteコンポーネントにPropsにrenderを渡すことができる。renderにアロー関数を設定し、その中でコンポーネントを返却するとレンダリングするコンポーネントを指定することができる。
renderはデフォルトでPropsを受け取っている。その中身はルーティングに関する情報を持ったオブジェクトである。プロパティとしてhistory/location/matchのオブジェクトとstaticContextを持っている。例えば、renderオブジェクトの中のmatchオブジェクトのurlプロパティを参照するとURLの相対パスを受け取ることができる。
Switchは入れ子にできる。この場合、子のRouteコンポーネントpathを親のRouteコンポーネントに指定したpathをルートとした記述とすることができる。
別ファイルに配列として切り出したパス設定を呼び出してループしてルーティングする事もできる。
useParamsというHooksを使用することで、URLパラメーターを受け取ることができる。
useLocationというHooksを使用することで、searchとしてURLクエリパラメーターを受け取ることができる。
useHistoryというHooksを使用することで、javascriptを使用しての画面遷移を簡単に扱うことができる。

コンポーネント分割(Atomic Design)

Atomic Designとは

  • 画面要素を5段階に分け、組み合わせることでUIを実現
  • コンポーネント化された要素が画面を構成しているという考え方
  • React用ではなく、もともとはデザインの考え方
  • モダンjavascript(コンポーネント)と相性が良い

5段階のコンポーネント

atomic-design.png
名称役割
ATOMS最小でそれ以上分解できない要素
例:ボタン/入力テキストボックス/アイコン 等
MOLECULESAtomの組み合わせで意味を持つデザインパーツ
例:アイコン+メニュー名/プロフィール画像+テキストボックス/アイコンセット 等
ORGANISMSAtomやMoleculeの組み合わせで構成される、単体である程度の意味を持つ要素群
例:入力エリア/サイドメニュー 等
TEMPLATESページのレイアウトのみを表現する要素(実際のデータは持たない)
例:ヘッダー、サイドメニュー、コンテンツエリア等のレイアウト情報 等
PAGES最終的に表示される1画面
例:ページ遷移毎に表示される各画面

ディレクトリ構成の例
atomicdesigncomponents.png

まとめ

あくまでベース

Atomic Designはあくまで概念だと認識し、プロジェクトやチームに合わせてカスタマイズしていく。
Atomic Designをすることが目的になってはいけない。
プロジェクトの初期段階でチーム内で取り決めるのがベターである。

はじめから無理して分けない

慣れないうちに無理にコンポーネントに分けようとするとしんどい。
まずは書いて定期的にリファクタリング(コンポーネント分割)する。

要素の関心を意識

「何に関心があるコンポーネントなのか」を意識しながら分割したり、Propsを定義したりする。

グローバルなstate管理(Context/Recoil)

グローバルなstateとは

画面のどのコンポーネント/どのページからでも参照できるような値と、その値をどのページからでも更新していけるようなイメージ

Context

import React, { createContext, useState } from "react";
// Contextを作成するためにcreateContextを使用する。他の画面でも参照できるようにexportする。
export const UserContext = createContext({});

// 以下Contextを扱う上でのお作法のようなもの
export const UserProvider = (props) => {
  const { children } = props;
  const [userInfo, setUserInfo] = useState(null);

  return (
    <UserContext.Provider value={{ userInfo, setUserInfo }}> <!-- 作成したContextの中にはProviderがあるので返却する。グローバルに参照したい値をvalueに渡す。そうすることで、Providerで囲った配下の要素の中で参照できるようになる -->
      {children} <!-- どんな要素でも囲えるようにPropsとしてchildrenを受け取れるようにしておく -->
    </UserContext.Provider>
  );
};

import { UserProvider } from "./providers/UserProvider";
import { Router } from "./router/Router";
import "./styles.css";

export default function App() {
  return (
      <UserProvider> <!-- 作成したProviderで囲ってあげる -->
        <Router />
      </UserProvider>
  );
}

import React, { memo } from "react";

import styled from "styled-components";
import { UserContext } from "../../../providers/UserProvider";

export const UserIconWithName = memo((props) => {
  console.log("UserIconWithName");
  const { image, name } = props;
  //
 ContextはuseContextというHooksを使用することで参照できる。引数に参照したいContextを指定する
  const { userInfo } = useContext(UserContext);

  const isAdmin = userInfo ? userInfo.isAdmin : false;

  return (
    <SContainer>
      <SImage height={160} width={160} src={image} alt={name} />
      <SName>{name}</SName>
      {isAdmin && <SEdit>編集</SEdit>}
    </SContainer>
  );
});

const SContainer = styled.div`
  text-align: center;
`;

const SImage = styled.img`
  border-radius: 50%;
`;

const SName = styled.p`
  font-size: 18px;
  font-weight: bold;
  margin: 0;
  color: #40514e;
`;

const SEdit = styled.span`
  text-decoration: underline;
  color: #aaa;
  cursor: pointer;
`;

import styled from "styled-components";

import { SecondaryButton } from "../atoms/button/SecondaryButton";
import { useHistory } from "react-router-dom";
import { useSetRecoilState } from "recoil";
import React from "react";
import { UserContext } from "../../providers/UserProvider";

export const Top = () => {
  const history = useHistory();
  //
 Context(グローバルなState)更新のために、UserContextからStateの更新関数setUserInfoを受け取る
  const { setUserInfo } = useContext(UserContext);

  const onClickAdmin = () => {
    setUserInfo({ isAdmin: true });
    history.push("/users");
  };

  const onClickGeneral = () => {
    setUserInfo({ isAdmin: false });
    history.push("/users");
  };

  return (
    <Scontainer>
      <h2>TOPページです</h2>
      <SecondaryButton onClick={onClickAdmin}>管理者ユーザー</SecondaryButton>
      <br />
      <br />
      <SecondaryButton onClick={onClickGeneral}>一般ユーザー</SecondaryButton>
    </Scontainer>
  );
};

const Scontainer = styled.div`
  text-align: center;
`;

Recoil

Recoilとは

グローバルなState管理ができるライブラリ。手軽にStateの管理をすることができる。
RecoilでState管理をする場合は、storeというディレクトリを切ってその中で管理していくのが一般的である。

// recoilでState管理をする場合、atomを使用する
import { atom } from "recoil";

export const userState = atom({
  key: "userState", // 一意のキー(ファイル名と揃えるのが自然)
  default: { isAdmin: false } // デフォルトの値を設定
});

import { RecoilRoot } from "recoil";

import { UserProvider } from "./providers/UserProvider";
import { Router } from "./router/Router";
import "./styles.css";

export default function App() {
  return (
    <RecoilRoot> <!-- Recoilで作成したStateを扱う場合、RecoilRootで囲ってあげる必要がある -->
      <UserProvider>
        <Router />
      </UserProvider>
    </RecoilRoot>
  );
}

import React from "react";

import styled from "styled-components";
// import { UserContext } from "../../providers/UserProvider";
import { SecondaryButton } from "../atoms/button/SecondaryButton";
import { SearchInput } from "../molecules/SearchInput";
import { UserCard } from "../organisms/user/UserCard";
import { useRecoilState } from "recoil";
import { userState } from "../../store/userState";

const users = [...Array(10).keys()].map((val) => {
  return {
    id: val,
    name: `じゃけぇ-${val}`,
    image: "https://source.unsplash.com/NE0XGVKTmcA",
    email: "12345@example.com",
    phone: "090-1111-2222",
    company: {
      name: "テスト株式会社"
    },
    website: "https://google.com"
  };
});

export const Users = () => {
  // const { userInfo, setUserInfo } = useContext(UserContext);

  // RecoilのStateを参照する場合、useRecoilStateを使用する(useStateと同じ感覚で使える)
  const [userInfo, setUserInfo] = useRecoilState(userState);
  const onClickSwitch = () => setUserInfo({ isAdmin: !userInfo.isAdmin });
  return (
    <Scontainer>
      <h2>ユーザー一覧</h2>
      <SearchInput />
      <br />
      <SecondaryButton onClick={onClickSwitch}>切り替え</SecondaryButton>
      <SUserArea>
        {users.map((user) => (
          <UserCard key={user.id} user={user} />
        ))}
      </SUserArea>
    </Scontainer>
  );
};

const Scontainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px;
`;

const SUserArea = styled.div`
  padding-top: 40px;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  grid-gap: 20px;
`;

import React, { memo } from "react";

import styled from "styled-components";
import { useRecoilValue } from "recoil";
// import { UserContext } from "../../../providers/UserProvider";
import { userState } from "../../../store/userState";

export const UserIconWithName = memo((props) => {
  console.log("UserIconWithName");
  const { image, name } = props;
  // const { userInfo } = useContext(UserContext);
  // RecoilのStateの値だけを参照する場合、useRecoilValueを使用する
  const userInfo = useRecoilValue(userState);
  const isAdmin = userInfo ? userInfo.isAdmin : false;

  return (
    <SContainer>
      <SImage height={160} width={160} src={image} alt={name} />
      <SName>{name}</SName>
      {isAdmin && <SEdit>編集</SEdit>}
    </SContainer>
  );
});

const SContainer = styled.div`
  text-align: center;
`;

const SImage = styled.img`
  border-radius: 50%;
`;

const SName = styled.p`
  font-size: 18px;
  font-weight: bold;
  margin: 0;
  color: #40514e;
`;

const SEdit = styled.span`
  text-decoration: underline;
  color: #aaa;
  cursor: pointer;
`;

import styled from "styled-components";

import { SecondaryButton } from "../atoms/button/SecondaryButton";
import { useHistory } from "react-router-dom";
import { useSetRecoilState } from "recoil";
import React from "react";
// import { UserContext } from "../../providers/UserProvider";
import { userState } from "../../store/userState";

export const Top = () => {
  const history = useHistory();
  // const { setUserInfo } = useContext(UserContext);
  // RecoilのStateのset関数だけを使用する場合、useSetRecoilStateを使用する。
  const setUserInfo = useSetRecoilState(userState);
  const onClickAdmin = () => {
    setUserInfo({ isAdmin: true });
    history.push("/users");
  };

  const onClickGeneral = () => {
    setUserInfo({ isAdmin: false });
    history.push("/users");
  };

  return (
    <Scontainer>
      <h2>TOPページです</h2>
      <SecondaryButton onClick={onClickAdmin}>管理者ユーザー</SecondaryButton>
      <br />
      <br />
      <SecondaryButton onClick={onClickGeneral}>一般ユーザー</SecondaryButton>
    </Scontainer>
  );
};

const Scontainer = styled.div`
  text-align: center;
`;

まとめ

グローバルなStateは、バケツリレー(親コンポーネントのStateを孫コンポーネントで扱いたい場合、親コンポーネント->子コンポーネント->孫コンポーネントの経由をする必要がある)をしなくて済むことがメリットである。

Contextはprovidersというディレクトリを切ってその中で管理していくのが一般的である。
Contextを作成するためにcreateContextを使用する。他の画面でも参照できるようにexportする。
作成したContextの中にはProviderがあるので返却する。グローバルに参照したい値をvalueに渡す。そうすることで、Providerで囲った配下の要素の中で参照できるようになる。
どんな要素でも囲えるようにProviderのPropsとしてchildrenを受け取れるようにしておく。
ContextはuseContextというHooksを使用することで参照できる。引数に参照したいContextを指定する。

RecoilでState管理をする場合は、storeというディレクトリを切ってその中で管理していくのが一般的である。
RecoilのStateを参照する場合、useRecoilStateを使用する(useStateと同じ感覚で使える)。
RecoilのStateの値だけを参照する場合、useRecoilValueを使用する。
RecoilのStateのset関数だけを使用する場合、useSetRecoilStateを使用する。

カスタムフック

カスタムフックとは

  • ただの関数
  • hooksの各機能を使用できる
  • コンポーネントからロジックを分離していく考え方
  • メリットとして使い回しがきく、テストが容易、見通しが良くなる等がある
  • 自由に作成できる。暗黙のルールでuse~という命名にする

import axios from "axios";

import { useState } from "react";
import { UserProfile } from "../types/userProfile";
import { User } from "../types/api/user";

// 全ユーザー一覧を取得するカスタムフック
export const useAllUsers = () => {
  const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const getUsers = () => {
    setLoading(true);
    setError(false);

    axios
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        const data = res.data.map((user) => ({
          id: user.id,
          name: `${user.name}(${user.username})`,
          email: user.email,
          address: `${user.address.city}${user.address.suite}${user.address.street}`
        }));
        setUserProfiles(data);
      })
      .catch((err) => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  // 他のコンポーネントでカスタムフックで定義したStateや関数が使用できるように、returnで返却する
  return { getUsers, userProfiles, loading, error };
};

import "./styles.css";

import { UserCard } from "./components/UserCard";
import { useAllUsers } from "./hooks/useAllUsers";

export default function App() {
  // カスタムフックを呼び出し、返却値を受け取る
  const { getUsers, userProfiles, loading, error } = useAllUsers();
  const onClickFetchUser = () => getUsers();

  return (
    <div className="App">
      <button onClick={onClickFetchUser}>データ取得</button>
      <br />
      {error ? (
        <p style={{ color: "red" }}>データの取得に失敗しました</p>
      ) : loading ? (
        <p>Loading...</p>
      ) : (
        <>
          {userProfiles.map((user) => (
            <UserCard key={user.id} user={user} />
          ))}
        </>
      )}
    </div>
  );
}

まとめ

カスタムフックはhooksというディレクトリを切って管理していくのが一般的である。
他のコンポーネントでカスタムフックで定義したStateや関数が使用できるように、returnで返却する。
カスタムフックで呼び出したStateはそれぞれのコンポーネントで独立扱いなので、コンポーネント間でStateは競合しない。

おわりに

Reactはjsx記法が特殊ですが、慣れればコーディングが快適になるのでいいですね!

次回はReact×TypeScriptについて学習していきたいと思います!

それでは!

この記事を書いた人

棒人間

入社年2019年

出身地東京都

業務内容開発

特技または趣味スケボー

棒人間の記事一覧へ

テクログに関する記事一覧