Closed55

Reactのメモ

kohskikohski

propsとchildren

子コンポーネント

components/ColorfulMessage.tsx
import React, { Component } from "react";

export const ColorfuleMessage = (props: {
  color: string;
  children: Component | string;
}) => {
  // 子コンポーネントの属性はそのまま、子コンポーネントのInnerHTMはchildrenで受ける
  // 単一の文字列だけじゃなくて、JSXを受けることも可能
  const { color, children } = props;
  const pStyle = { color: color, backgroundColor: "teal", fontSize: "30px" };
  return <p style={pStyle}>{children}</p>;
};

親コンポーネント

App.tsx
import React from "react";
import { ColorfuleMessage } from "./components/ColorfulMessage";

const App = () => {
  const onClickButton = () => {
    alert();
  };

  // styleはjsのobject形式でinline定義が可能
  const h1Style = { color: "blue", backgroundColor: "gray", fontSize: "30px" };

  return (
    <>
      <h1 style={h1Style}>こんにちは!</h1>
      {/* props.color = blue, children=お元気ですか?になる */}
      <ColorfuleMessage color="blue">お元気ですか?</ColorfuleMessage>
      <ColorfuleMessage color="pink">元気です</ColorfuleMessage>
    </>
  );
};

export default App;

kohskikohski

useState

サンプル

import React, { useState, useEffect } from "react";

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

  const countUp = () => {
    setNum(num + 1);
  };
  return (
    <>
      <button onClick={countUp}>countup</button>
      <p>{num}</p>
    </>
  );
};

export default App;

確認

  • 子コンポーネントで同名の変数/更新関数を定義したuseStateを使ってみたが、状態は同期されない。
  • おそらく親コンポーネントで定義して、propsでバケツリレーするっぽい。

確認事項

  • グローバルに使用したい状態はどうやって扱うんだろう?ログイン, ダークモードなど?
kohskikohski

userEffect

component再レンダリングの条件

  1. stateの変更
  2. 受け取っているpropsの変更
  3. 親コンポーネントの変更

無限レンダリングの例

App.tsx
import React, { useState, useEffect } from "react";

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

  // setFaceShowFlag()でfaceShowFlagが変更された段階で、この関数の上部へ戻る
  // またここへ来て、setFaceShowFlag()が呼ばれる
  if (num > 0 && num % 3 === 0) {
    setFaceShowFlag(true);
  } else {
    setFaceShowFlag(false);
  }

  return (
    <>
      <button onClick={countUp}>countup</button>
      <p>{num}</p>
      {/* 以下のようにfaceShowFlagがtrueなら...は論理積で表現するのが良さそう。 */}
      {faceShowFlag && <p>m(__)m</p>}
      <DisplayNum />
    </>
  );
};

export default App;

解決策余計な状態変化をif文で減らす

App.tsx
if (num > 0 && num % 3 === 0) {
   faceShowFlag || setFaceShowFlag(true);
  } else {
    faceShowFlag && setFaceShowFlag(false);
  }

useEffectを使う

  1. useEffectの第二引数に[]を入れると初回のみレンダリング
  2. [num]にするとnumに変化があったときだけuseEffectの中身が実行される。
App.tsx
useEffect(() => {
  console.log("use effect");
  // 以下のコードは無限ループになってしまう
  if (num > 0 && num % 3 === 0) {
    // faceShowFlagを変更する必要がない時はsetしないっていうので 対処可能
    faceShowFlag || setFaceShowFlag(true);
  } else {
    faceShowFlag && setFaceShowFlag(false);
  }
  // eslint-disable-next-line
}, [num]);
kohskikohski

Input要素の処理

App.tsx
import React, { useState } from "react";
import "./styles.css";

export const App = () => {
  const [todoText, setTodoText] = useState("");
  const [incompleteTodos, setIncompleteTodos] = useState(["todo1", "todo2"]);

  // 3. 型はhttps://zenn.dev/koduki/articles/0f8fcbc9a7485bを参照
  const onCgangeTodoText = (event: { target: HTMLInputElement }) => {
    setTodoText(event.target.value);
  };

  const onClickAdd = () => {
    // 4. 文字列がない時のガード
    if (!todoText) {
      return;
    }
    // 5. 既存のtodoを展開して、末尾に入力値を追加
    const newTodos = [...incompleteTodos, todoText];
    // 6. それで更新
    setIncompleteTodos(newTodos);
    // 7. inputBoxを初期化
    setTodoText("");
  };

  return (
    <>
      <div className="input-area">
        <input
          placeholder="TODOを入力"
          // 1. valueでstateのtodoTextを参照
          value={todoText}
          // 2. onChangeイベントをハンドルする
          onChange={onCgangeTodoText}
        />
        <button onClick={onClickAdd}>追加</button>
      </div>
      <div className="incomplete-area">
        <p className="title">未完了のTODO</p>
        <ul>
          {/* 8.JSの世界でloopする */}
          {incompleteTodos.map((todo) => {
            return (
              // 9. keyを設定することで仮想DOMにどの要素が変更されたかを正確に伝えることが可能
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button>完了</button>
                <button>削除</button>
              </div>
            );
          })}
        </ul>
      </div>
    </>
  );
};
kohskikohski

input要素の型定義はこっちが正しい!

適当.tsx
import { ChangeEvent, FC, memo, useState } from "react";

import { PrimaryButton } from "../atoms/button/PrimaryButton";

import { Flex, Heading, Box, Divider, Input, Stack } from "@chakra-ui/react";

export const Login: FC = memo(() => {
  const [userId, setUserId] = useState("");

  // テキストボックスのイベントの型定義
  const onChangeUserId = (e: ChangeEvent<HTMLInputElement>) => {
    setUserId(e.target.value);
  };

  return (
    <>
      <Flex align="center" justify="center" height="100vh">
        <Box bg="white" w="sm" borderRadius="md" shadow="md">
          <Heading as="h1" size="lg" textAlign="center">
            ユーザー管理アプリ
          </Heading>
          <Divider />
          <Stack spacing={4} py={4} px={10}>
            <Input
              placeholder="ユーザーID"
              value={userId}
              onChange={onChangeUserId}
            />
            <PrimaryButton>ログイン</PrimaryButton>
          </Stack>
        </Box>
      </Flex>
    </>
  );
});
kohskikohski

よくある削除対応

App.tsx
import React, { useState } from "react";
import "./styles.css";

export const App = () => {
  const [incompleteTodos, setIncompleteTodos] = useState(["todo1", "todo2"]);

  const onClickDelete = (index: number) => {
    // 3. JSはオブジェクトが参照渡しなので、配列のコピーを忘れないようにする
    const newTodos = [...incompleteTodos];
    newTodos.splice(index, 1);
    setIncompleteTodos(newTodos);
  };

  return (
    <>
      <div className="incomplete-area">
        <p className="title">未完了のTODO</p>
        <ul>
          {/* 1. 削除対象がわかるようにmapの第二引数にindexを追加 */}
          {incompleteTodos.map((todo, index) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button>完了</button>
                {/* 2. onClickイベントのハンドラーにindexを渡す
                  ただし onClick={onClickDelete(index)} とすると即時実行されてしまう
                  そのため、nClick={() => onClickDelete(index)}でアロー関数を新規に作成する
                */}
                <button onClick={() => onClickDelete(index)}>削除</button>
              </div>
            );
          })}
        </ul>
      </div>
    </>
  );
};
kohskikohski

レンダリング最適化

  1. memo関数を使う
  2. useCallback関数を使う
  3. useMemo関数を使う
kohskikohski

レンダリングの最適化1 ... memo関数を使う

基本的には子コンポーネントは全部memo()でも良いかも。
でも、memo()自体のコストもあり、再描画の負担が小さいのなら良いのではないか。

コード例

ChildComponent.tsx
import { memo } from "react";

export const ChildComponent = memo((props: { open: boolean }) => {
  console.log("rendering in child");
  const { open } = props;

  // 擬似的に重くする処理
  const data = [...Array(2000).keys()];
  data.forEach(() => {
    console.log("...");
  });

  return <>{open ? <p>子コンポーネント</p> : null}</>;
});

確認事項

  • memo()って何やってるんやろう?
kohskikohski

レンダリングの最適化2 ... useCallback関数を使う

子コンポーネントをmemo()でメモ化してもpropsでcallback関数を渡していた場合、そのcallback関数が都度作成されるので、stateの更新と見做され子コンポーネントの再レンダリングが走ってしまう。

useCallback(() => setState(), [])

コード例

App.tsx
import React, { useState, useCallback } from "react";
import { ChildComponent } from "./components/ChildComponent";

export const App = () => {
  const [open, setOpen] = useState(false);

  const onClickSomeFunc = useCallback(() => {
    setOpen(false);
  }, []);

  return (
    <div className="App">
      {/* 
        ChildComponent自体はmemo化されているが、propsに関数が含まれる場合は、
        関数が都度新規に作られているので、Stateの更新とみなされる。
        useCallbackを使用してpropsで渡す関数自体もメモ化する必要がある。
      */}
      <ChildComponent open={open} onClickClose={onClickSomeFunc} />
    </div>
  );
};
kohskikohski

レンダリングの最適化3 ... useMemo関数を使う

値の計算についてもレンダリングのたびに走ると困るものもある。
その場合はuseMemo()を使用する

  // const temp = (() => {
  //   console.log("計算"); // レンダリングの都度計算が走る
  //   return 100 * 3;
  // })();

  const temp = useMemo(() => {
    console.log("計算");
    return 1 + 3;
  }, []); // 初回一回で済む。
  console.log(temp);
kohskikohski

Styleの当て方

  1. inline style
  2. css module
  3. Styled Jsx
  4. Styled Component
  5. Emotion
kohskikohski

Styleの当て方1 ... inline style

Reactのネイティブな機能で実現可能。cssの属性をcamelケースにして、jsのobjectを渡す感じ。

よいところ

  • libraryを追加しないでいい

悪いところ

  • 普通のオブジェクトなので補完がきかない(?)
  • scssの擬似要素等には未対応

実装例

export const InlineStyle = () => {
  const containerSyle = {
    border: "solid 2px #392eff",
    borderRadius: "20px",
    padding: "8px",
    margin: "8px",
    display: "flex",
    justifyContent: "space-around",
    alignItems: "center"
  };
  const titleSyle = {
    margin: 0,
    color: "#3d84a8"
  };
  const buttonSyle = {
    backgroundColor: "#abedd8",
    border: "none",
    padding: "8px",
    borderRadius: "8px"
  };

  return (
    <div style={containerSyle}>
      <p style={titleSyle}>- Inline Styles -</p>
      <button style={buttonSyle}>Fight!!</button>
    </div>
  );
};

kohskikohski

Styleの当て方2 ... css module

よいところ

  • scssそのまま使える
  • module単位で適用するので名前衝突しにくそう

悪いところ

  • fileを分けるので管理めんどくさそう

実装例

Component.tsx
import * as styles from "./component.module.scss";

export const Component = () => {
  return (
    <div className={styles.container}>
      <p className={styles.title}>- Css Modules -</p>
      <button className={styles.button}>Fight!!</button>
    </div>
  );
};
component.module.scss
// css module単位でnamespaceが 切れるので、競合の心配がない
.container {
  border: solid 2px #392eff;
  border-radius: 20px;
  padding: 8px;
  margin: 8px;
  display: flex;
  justify-content: space-around;
  align-items: center;
}
.title {
  margin: 0;
  color: #3d84a8;
}

.button {
  background-color: #abedd8;
  border: none;
  padding: 8px;
  border-radius: 8px;
  // CSS Modulesを使えば、擬似要素等もscssの記述に則って記載できる
  &:hover {
    background-color: #46cdcf;
    color: #fff;
    cursor: pointer;
  }
}
kohskikohski

Styleの当て方3 ... Styled Jsx

特徴

  1. styled jsxはnext.jsに組み込みになっている
  2. そのままだと疑似要素使えないので注意が必要
  3. vs codeならコード補完やハイライトの拡張機能がある

実装例

StyledJsx.tsx
export const StyledJsx = () => {
  return (
    <>
      <div className="container">
        <p className="title">- Styled JSX -</p>
        <button className="button">Fight!!</button>
      </div>
      <style jsx="true">{`
        .container {
          border: solid 2px #392eff;
          border-radius: 20px;
          padding: 8px;
          margin: 8px;
          display: flex;
          justify-content: space-around;
          align-items: center;
        }
        .title {
          margin: 0;
          color: #3d84a8;
        }
        .button {
          background-color: #abedd8;
          border: none;
          padding: 8px;
          border-radius: 8px;
          &:hover {
            background-color: #46cdcf;
            color: #fff;
            cursor: pointer;
          }
        }
      `}</style>
    </>
  );
};

kohskikohski

Styleの当て方4 ... Styled Component

特徴

ぱっと見、Reactっぽい書き方ができそう。

StyledComponent.tsx
import styled from "styled-components";

// Styeled Componentなのか、importしたcomponentなのかが一瞥できない
const Container = styled.div`
  border: solid 2px #392eff;
  border-radius: 20px;
  padding: 8px;
  margin: 8px;
  display: flex;
  justify-content: space-around;
  align-items: center;
`;

const Title = styled.p`
  margin: 0;
  color: #3d84a8;
`;

const Button = styled.button`
  background-color: #abedd8;
  border: none;
  padding: 8px;
  border-radius: 8px;
  // 擬似要素もそのまま使える
  &:hover {
    background-color: #46cdcf;
    color: #fff;
    cursor: pointer;
  }
`;

export const StyledComponent = () => {
  return (
    <Container>
      <Title>- Styled Component -</Title>
      <Button>Fight!!</Button>
    </Container>
  );
};
kohskikohski

Styleの当て方5 ... Emotion

/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx, css } from "@emotion/react";
import styled from "@emotion/styled";

export const Emotion = () => {
  const containerStyle = css`
    border: solid 2px #392eff;
    border-radius: 20px;
    padding: 8px;
    margin: 8px;
    display: flex;
    justify-content: space-around;
    align-items: center;
  `;

  const titleStyle = css({
    margin: 0,
    color: "#3d84a8"
  });

  return (
    <div css={containerStyle}>
      <p css={titleStyle}>- Emotion -</p>
      <Button>Fight!!</Button>
    </div>
  );
};

const Button = styled.button`
  background-color: #abedd8;
  border: none;
  padding: 8px;
  border-radius: 8px;
  // CSS Modulesを使えば、擬似要素等もscssの記述に則って記載できる
  &:hover {
    background-color: #46cdcf;
    color: #fff;
    cursor: pointer;
  }
`;
kohskikohski

ReactRouterの使い方

いったんver.5系で進めるので注意。

確認事項

ver.5 => ver.6 について確認

kohskikohski

基本的な使い方

App.tsx
import { BrowserRouter, Link, Switch, Route } from "react-router-dom";

import { Home } from "./components/Home";
import { Page1 } from "./components/Page1";
import { Page2 } from "./components/Page2";

export default function App() {
  return (
    // 1. BrowserRouterで包括する
    // 2. index.tsxでBrowserRouterを指定しても問題なく動いた。
    <BrowserRouter>
      <div className="App">
        {/* 2. LinkタグでLinkを設置 */}
        <Link to="/">Home</Link>
        <br />
        <Link to="/page1">Page1</Link>
        <br />
        <Link to="/page2">Page2</Link>
        <br />
      </div>
      {/* 3. Switch内にRouteで指定したコンポーネントを描画 */}
      <Switch>
        {/* 4. "/"は部分一致だと全部マッチしてしまうので、exactで完全一致にできる */}
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/page1">
          <Page1 />
        </Route>
        <Route path="/page2">
          <Page2 />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

kohskikohski

ネストされたページ遷移

ルーティングの定義

App.tsx
import { BrowserRouter, Switch, Route } from "react-router-dom";

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

export default function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route
          path="/page1"
          /**
            1. Routerのrender属性でcomponentを返すアロー関数を定義
            render(() => {
              return <Page1>
            })
           */
          render={({ match: { url } }) => (
            // 2. そのアロー関数の中でSwitch, Routeを使ってNestを表現。
            // 3. renderに当てたアローが受け取るpropsにprops.match.urlで現在のpathが受け取れるのでそれを使って、子ページのpathを表現
            <Switch>
              <Route exact path={url}>
                <Page1 />
              </Route>
              <Route path={`${url}/detailA`}>
                <Page1DetailA />
              </Route>
              <Route path={`${url}/detailB`}>
                <Page1DetailB />
              </Route>
            </Switch>
          )}
        />
      </Switch>
    </BrowserRouter>
  );
}

ネスト起点のページ

Page1.tsx
import { Link } from "react-router-dom";

export const Page1 = () => {
  return (
    <div>
      <h1>Page1ページです</h1>
      <br />
      <Link to="/page1/detailA">DetailA</Link>
      <br />
      <Link to="/page1/detailB">DetailB</Link>
    </div>
  );
};
kohskikohski

ルーターのリファクタリング

  1. routes/Router.tsxにルーター定義を移動
  2. App.tsx内では<Router />だけを配置
  3. ネストされたページは必要な反復している要素を配列化 ※1 router/Page1Routes.tsx
  4. routes/Router.tsx内でループ処理でレンダリング
router/Page1Routes.tsx(※1)
import { Page1 } from "../components/Page1";
import { Page1DetailA } from "../components/Page1DetailA";
import { Page1DetailB } from "../components/Page1DetailB";

export const page1Routes = [
  {
    path: "/",
    exact: true,
    children: <Page1 />
  },
  {
    path: "/detailA",
    exact: false,
    children: <Page1DetailA />
  },
  {
    path: "/detailB",
    exact: false,
    children: <Page1DetailB />
  }
];
router/Route.tsx(※2)
import { Switch, Route } from "react-router-dom";

import { page1Routes } from "./Page1Routes";

export const Router = () => {
  return (
    <Switch>
      <Route
        path="/page1"
        render={({ match: { url } }: { match: { url: string } }) => (
          <Switch>
            {page1Routes.map((route) => {
              return (
                <Route
                  key={route.path}
                  path={`${url}${route.path}`}
                  exact={route.exact}
                >
                  {route.children}
                </Route>
              );
            })}
          </Switch>
        )}
      />
    </Switch>
  );
};

kohskikohski

パスパラメーターの取得

  • Router上は:<パラメーター名>( 例: :id)で受ける
  • 使用時には
    • useParamsを使用する

Routerの定義

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

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

export const Router = () => {
  return (
    <Switch>
      <Route
        path="/page2"
        render={({ match: { url } }: { match: { url: string } }) => (
          <Switch>
            {/* :idのように受ける! */}
            <Route path={`${url}/:id`} exact={true}>
              <UrlParameter />
            </Route>
          </Switch>
        )}
      />
    </Switch>
  );
};
components/UrlParameters.tsx
import { useParams } from "react-router-dom";

export const UrlParameter = () => {
  const { id } = useParams();
  return (
    <div>
      <h1>UrlPatemeterページです</h1>
      <p>パラメーターは{id}です</p>
    </div>
  );
};
kohskikohski

クエリストリングの取得

  • useLocationの返り値からsearchを取得
  • searchからnew URLSearchParamsでオブジェクトを作成
  • get('属性名')する
components/UrlParameter.tsx
import { useParams, useLocation } from "react-router-dom";

export const UrlParameter = () => {
  const { id } = useParams();
  const { search } = useLocation();
  console.log(search); // ?name=hogehoge
  const query = new URLSearchParams(search); // JS標準
  console.log(query.get("name")); // hogehoge
  return (
    <div>
      <h1>UrlPatemeterページです</h1>
      <p>パラメーターは{id}です</p>
    </div>
  );
};
kohskikohski

Stateを画面遷移時に受け渡す

  • Linkタグのto属性にオブジェクトを渡す
  • { pathname: "<遷移先相対パス>", state: <オブジェクト> }

実装例

components/Page1.tsx(渡し元)
import { Link } from "react-router-dom";

export const Page1 = () => {
  // 仮に渡すオブジェクトを作成(APIの一覧データとか)
  const arr = [...Array(100).keys()];
  return (
    <div>
      <h1>Page1ページです</h1>
      <br />
      {/* toの中身を変更。pathnameしかない場合のショートハンドが`to=<遷移先相対パス>`だった。*/}
      <Link to={{ pathname: "/page1/detailA", state: arr }}>DetailA</Link>
      <br />
      <Link to="/page1/detailB">DetailB</Link>
    </div>
  );
};

kohskikohski

JavaScriptで画面遷移

  • useHistoryを使用する
  • history.push("相対パス")
  • history.goBack() ... 戻る
components/Page1.tsx
import { Link, useHistory } from "react-router-dom";

export const Page1 = () => {
  const history = useHistory();

  const onClickDetailA = () => {
    history.push("/page1/detailA");
  };

  const onClickGoback = () => {
    history.goBack();
  };

  return (
    <div>
      <h1>Page1ページです</h1>
      <button onClick={onClickDetailA}>DetailA</button>
      <br />
      <button onClick={onClickGoback}>戻る</button>
    </div>
  );
};

kohskikohski

404ページ

全部にマッチはRouter定義の末尾でto="*"で設定する。

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

import { Home } from "../components/Home";
import { Page404 } from "../components/Page404";

export const Router = () => {
  return (
    <Switch>
      <Route exact path="/">
        <Home />
      </Route>
      <Route to="*" render={Page404} />
    </Switch>
  );
};

kohskikohski

Atomi Design

  1. Atoms ... それ以上分解できないもの。
    • 例: ボタン、アイコン
  2. Molecuels ... Atomの組み合わせで意味をもつもの
    • 例: アイコン + メニュー名, テキストボックス + ボタン
  3. Organisms ... AtomやMoleculeの組み合わせで構成される単体である程度の意味をもつ要素群
    • ツイートエリア
  4. Templates ... ページのレイアウトのみを表現する要素
    • データをもたない
  5. Pages ... 最終的に表示される1画面
kohskikohski

Atomsの実装

ディレクトリ構成

実装

  • ディレクトリを切る
  • styledComponentsはBaseを定義して、継承するようなやり方がある。
components/atoms/buttons/BaseButton.tsx
import styled from "styled-components";

export const BaseButton = styled.button`
  color: #ffffff;
  padding: 6px 24px;
  border: none;
  outline: none;
  border-radius: 9999px;
  &:hover {
    cursor: pointer;
    opacity: 0.8;
  }
`;
components/atoms/buttons/PrimaryButton.tsx
import styled from "styled-components";
import { BaseButton } from "./BaseButton";

const SButton = styled(BaseButton)`
  background-color: #40514e;
`;

export const PrimaryButton = (props: { children: any }) => {
  const { children } = props;
  return <SButton>{children}</SButton>;
};

kohskikohski

Moleculesの実装

ここでマージンやパディングのレイアウトを持たせるのが良さそう。

components/molecules/SearchInput.tsx
import styled from "styled-components";
import { PrimaryButton } from "../atoms/button/PrimaryButton";
import { Input } from "../atoms/input/Input";

const SButtonWrapper = styled.div`
  padding-left: 8px;
`;

const SContainer = styled.div`
  display: flex;
  align-items: center;
`;

export const SearchInput = () => {
  return (
    <div>
      <SContainer>
        <Input placeholder="検索条件を入力" />
        <SButtonWrapper>
          <PrimaryButton>検索</PrimaryButton>
        </SButtonWrapper>
      </SContainer>
    </div>
  );
};
kohskikohski

Layoutの実装

ディレクトリ構成

実装

components/templates/DefaultLayout.tsx
import { Header } from "../atoms/layout/Header";
import { Footer } from "../atoms/layout/Footer";

export const DefaultLayout = (props: { children: any }) => {
  const { children } = props;
  return (
    <>
      <Header></Header>
      {children}
      <Footer />
    </>
  );
};
components/atoms/layout/Header.tsx
import { Link } from "react-router-dom";

import styled from "styled-components";

const SHeader = styled.header`
  background-color: #11999e;
  color: #fff;
  text-align: center;
  padding: 8px 0;
`;

const SLink = styled(Link)`
  margin: 0 8px;
`;

export const Header = () => {
  return (
    <SHeader>
      <SLink to="/">Home</SLink>
      <SLink to="/users">Users</SLink>
    </SHeader>
  );
};
components/atoms/layout/Footer.tsx
import { Link } from "react-router-dom";

import styled from "styled-components";

const SFooter = styled.footer`
  background-color: #11999e;
  color: #fff;
  text-align: center;
  padding: 8px 0;
  position: fixed;
  bottom: 0;
  width: 100%;
`;

export const Footer = () => {
  return <SFooter>$copy; 2022 test Inc.</SFooter>;
};
App.tsx
import "./styles.css";

import { PrimaryButton } from "./components/atoms/button/PrimaryButton";
import { SecondaryButton } from "./components/atoms/button/SecondaryButton";
import { SearchInput } from "./components/molecules/SearchInput";
import { UserCard } from "./components/organisms/user/UserCard";
import { DefaultLayout } from "./components/templates/DefaultLayout";
import { BrowserRouter } from "react-router-dom";


export default function App() {
  return (
    <BrowserRouter>
      <DefaultLayout>  {/* <= Layout指定 */}
        <PrimaryButton>テスト</PrimaryButton>
        <SecondaryButton>検索</SecondaryButton>
        <br />
        <SearchInput></SearchInput>
        <UserCard user={user} />
      </DefaultLayout>  {/* <= Layout指定 */}
    </BrowserRouter>
  );
}

kohskikohski

コツ

  1. 意固地にならない。プロジェクトにより柔軟に対応
  2. 最初から分けない。まずは作ってリファクタリング
  3. 要素の関心を意識
kohskikohski

Contextを用いたGlobalState管理

Provider, Stateの定義

  1. providers/UserProvier.tsxを作成
  2. createContext()でContextを作成
  3. ProviderComponentを作成(例: UserProvider)
  4. Providerを使用するComponentをWrapする

Stateの使用

  1. const { userInfo, setUserInfo } = useContext(UserContext);
kohskikohski

Provider, Contextの定義

定義

providers/UserProvider.tsx
import React, { createContext, useState } from "react";

export const UserContext = createContext<{
  userInfo: { isAdmin: boolean };
  setUserInfo: any;
}>({
  userInfo: { isAdmin: false },
  setUserInfo: () => {}
});

export const UserProvider = (props: any) => {
  const { children } = props;

  const [userInfo, setUserInfo] = useState({ isAdmin: false });

  return (
    <UserContext.Provider value={{ userInfo, setUserInfo }}>
      {children}
    </UserContext.Provider>
  );
};

Wrapする

App.tsx
import "./styles.css";

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

export default function App() {
  return (
    <UserProvider>
      <Router />;
    </UserProvider>
  );
}
kohskikohski

使用時

適当.tsx
import { useContext } from "react";
import { UserContext } from "../../providers/UserProvider";

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

  const onClickSwitch = () => setUserInfo({ isAdmin: !userInfo.isAdmin });
  return (
    <div>
      { userInfo }
    </div>
  );
};

kohskikohski

再レンダリングに注意

Contextで値を変更すると参照しているコンポーネントが全て再レンダリングされる。
影響範囲を調べてmemo化することを忘れないようにする。

kohskikohski

Recoilを使用したグローバルステート管理

  1. store/UserStaet.tsを定義。(コンポーネント作らないので.tsでOK)
  2. 使用する
    • 参照と更新する
    • 参照だけする
    • 更新だけする(再レンダリング予防)
kohskikohski

定義

store/UserState.ts
import { atom } from "recoil";

export const userState = atom({
  key: "userState",
  default: { isAdmin: false }
});
kohskikohski

使用する

参照と更新

適当なcomponent.tsx
import { useRecoilState } from "recoil";
import { userState } from "../../store/UserState";

export const Users = () => {
  const [userInfo, setUserInfo] = useRecoilState(userState);

  const onClickSwitch = () => {
    setUserInfo({ isAdmin: !userInfo.isAdmin });
  };
  return <div>{userInfo}</div>;
};

参照のみ

適当.tsx
import { UserContext } from "../../../providers/UserProvider";
import { userState } from "../../../store/UserState";
const userInfo = useRecoilValue(userState);

更新のみ

更新のみの場合は、State更新しても再レンダリングは走らない。

適当.tsx
import { useSetRecoilState } from "recoil";
import { userState } from "../../store/UserState";
const setUserInfo = useSetRecoilState(userState);
kohskikohski

Reactでよく使う型

  1. VFC, FC
  2. Pick, Omit
  3. ReactNode
kohskikohski

VFC, FC

関数コンポーネントの型
-FC<propsの型>で定義するが、FCの場合childrenが暗黙のうちに含まれている。 childrenをとるコンポーネントもあればとらないコンポーネントもあるのでよくない。

  • VFC<propsの型>ではchildrenは明示的に定義する必要がある。
  • react v18で変更が入るかも
components/Todo.tsx
import { VFC } from "react";

import { TodoType } from "../types/todo";

export const Todo: VFC<Omit<TodoType, "id">> = (
  props: Omit<TodoType, "id">
) => {
  const { title, userId, completed = false } = props;
  const completeMark = completed ? "[完]" : "[未]";
  return <p>{`${completeMark} ${title}(ユーザー: ${userId})`}</p>;
};
kohskikohski

Omit, Pick

Type定義をまとめる

types/todo.ts
export type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

呼び出しコンポーネントでは一部しか使用しない

components/Todo.tsx
import { TodoType } from "../types/todo";
// 一部利用の型指定
props: Pick<TodoType, "userId" | "title" | "completed">
// いらないものを指定
props: Omit<TodoType, "id">

kohskikohski

オプショナルチェイニング

import { VFC } from "react";
import { User } from "../types/user";

type Props = {
  user: User;
};

export const UserProfile: VFC<Props> = (props: Props) => {
  const { user } = props;
  return (
    <dl>
      <dt>名前</dt>
      <dd>{user.name}</dd>
      <dt>趣味</dt>
      {/* OptionalChainingでhobbies: undefine時にも対応 */}
      <dd>{user.hobbies?.join(", ")}</dd>
    </dl>
  );
};
kohskikohski

ライブラリの型定義

  • index.d.tsがオブジェクトルートにあれば型定義を包含 ... axios
  • @types/が必要 ... react-router-dom
kohskikohski

childrenを受け取る時にどうするか

ReactNodeってのが最適

適当なコンポーネント
import { ReactNode, FC, memo } from "react";
import { Header } from "../organisms/layout/Header";

type Props = {
  children: ReactNode;
};

export const HeaderLayout: FC<Props> = memo((props: Props) => {
  const { children } = props;
  return (
    <>
      <Header />
      {children}
    </>
  );
});
kohskikohski

# カスタムフック

  • ただの関数
  • hooksの各機能を使用
  • コンポーネントからロジックを分離
  • 使い回し、テスト容易、見通しよくなる
  • use~と命名する
kohskikohski

カスタムフックス

  • フック内にロジックと状態を閉じ込めることができる。
  • 使用する側のコードがかなりシンプルになる。
hooks/useAllUsers.ts
// 全ユーザー一覧を取得するカス

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<UserProfile[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);
  const getUsers = () => {
    setLoading(true);
    setError(false);
    axios
      .get<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(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };
  return {
    getUsers,
    userProfiles,
    loading,
    error
  };
};
App.ts
import "./styles.css";

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

export default function App() {
  const { userProfiles, loading, error, getUsers } = useAllUsers();
  const onClickFetchUser = async () => getUsers();
  return (
    <div className="App">
      <button onClick={onClickFetchUser}>データ取得</button>
      <br />
      {error ? (
        <p style={{ color: "red" }}>データの取得に失敗しました</p>
      ) : loading ? (
        <p>ローディング中です。</p>
      ) : (
        <>
          {userProfiles.map((user) => (
            <UserCard user={user} key={user.id} />
          ))}
        </>
      )}
    </div>
  );
}

kohskikohski

ReactRouter v5 => v6

  • Switch => Routes
  • useHistory => useNavigate

参考

https://dev.classmethod.jp/articles/react-router-5to6/

kohskikohski

Switch => Routes

  1. SwitchRouteで置き換え
  2. 描画するコンポーネントはelementで受ける
  3. Layoutの描画部分は<Outlet />でうける
router/Routes.tsx
import { FC, memo } from "react";
import { Route, Routes } from "react-router-dom";
import { Login } from "../components/pages/Login";
import { Home } from "../components/pages/Home";
import { UserManagement } from "../components/pages/UserManagement";
import { Setting } from "../components/pages/Setting";
import { Page404 } from "../components/pages/Page404";
import { HeaderLayout } from "../components/template/HeaderLayout";

export const Router: FC = memo(() => {
  return (
    <Routes>
      <Route path="/" element={<Login />}></Route>
      <Route path="/home" element={<HeaderLayout />} >
        <Route path="" element={<Home />} />
        <Route path="user_management" element={<UserManagement />} />
        <Route path="setting" element={<Setting />} />
      </Route>
      <Route path="*" element={<Page404 />} />
    </Routes>
  );
});
components/templates/HeaderLayout.tsx
import { FC, memo } from "react";
import { Outlet } from "react-router-dom";
import { Header } from "../organisms/layout/Header";

export const HeaderLayout: FC = memo(() => {
  return (
    <>
      <Header />
      <Outlet />
    </>
  );
});
このスクラップは2022/10/07にクローズされました