Lambdaあれこれ

ファンアウト

ファンアウトパターンとは

処理を呼び出すプロセスから直接各処理を呼び出すのではなく、
間にノーティフィケーション(通知)コンポーネント
キューイングコンポーネントを入れることで
非同期かつ並列に処理が可能になる。
処理を呼び出すプロセスは、通知先への通知の後に処理を続行できる。
また、通知先の処理については知っている必要がないため処理を増やしたい場合も
通知コンポーネントへの通知先登録を増やせば対応できる。
また、キューごとに処理を割り当てて動作させれば並列に処理を実行できる。

通知コンポーネントSNS
キューイングコンポーネント:SQS

SQS(Amazon Simple Queue Service)

AWSが提供するメッセージキューイングサービス。
コンポーネント同士を分離し、スケールアウトをサポート。
LambdaのサポートによりSQSのエンキュー(キューにデータを入れること)をトリガーにLambdaを起動し、Lambdaが正常に終了すればメッセージは自動的に削除してくれる。
※トリガーとして対応しているキューは標準キューのみ(FIFOキューはポーリングが必要)

SNSAmazon Simple Notification Service)

AWSが提供するPub / Subメッセージングサービス
メッセージはプッシュ型で、トピックをサブスクライブできるのは以下
・Lambda、SQS、http/https、Eメール、SMS、アプリケーション

API Gateway

AWSが提供するバックエンドシステムへのAPIを作成するためのサービス
バックエンドはLambdaだけでなく、他のAWSサービスやHTTP(AWS以外でもパブリックに公開されているエンドポイントが存在するサービス)、Mockが選択できる。
LambdaをバックエンドにAPIを開発する際にはリクエストとレスポンスのマッピングを理解する必要がある。
API Gatewayでリクエストを受けてLambdaにどう渡すか、Lambdaでの処理結果をAPI Gatewayで利用するにあたりどう渡すかなど
SAMを使ってデプロイを行う場合、API GatewayとLambdaの統合にはLambdaプロキシ統合が利用される
Lambdaプロキシ統合ではカスタムのマッピングを利用することもできますが、それ以外ではフォーマットに従うことでマッピングが完了する
Lambda内でエラーハンドリング する際、エラーを返すとAPI Gatewayからクライアントへのレスポンスは常にMalformed Lambda proxy responseとなり、ステータスコードは502になる
本当に予期しないエラー発生時以外はLambdaからAPI Gatewayにエラーを返さないように注意する

DynamoDB

AWSが提供するNoSQLデータベースサービス
読み込み・書き込みのスペックを指定(購入)でき、スケーラビリティに優れている

RDSではなく、DynamoDBを使う場面
・DB同時接続の問題
Lambdaへのリクエストが多い時、同時実行制限数までLambdaのコンテナが作成、起動される
それから個別にDBにアクセスする際、DBの性能要件(CPU数、メモリー数)は接続に比例して大きくなっていく
DBが無限にスケールアップできるなら別

VPC内のリソースへのアクセス
RDSはVPC内に作成する一方で、LambdaはデフォルトではVPC内のリソースにアクセスできない
これを解決するためにENIをセットアップするのに10 ~ 30秒かかる
これらの制約からLambdaから接続するデータベースはDynamoDBが推奨される

useQueryについて

react-query

・サーバーからのデータ取得回数を減らす(fetch)
 (キャッシュに格納する)
・不要な再レンダリングを無くす

従来のstate管理

●State management(useState, redux store)
・Server data(REST APIから取得したデータ)
・React app state(ローカルのstate、isOpenなどのbooleanなど)
→Reduxのstoreに全てまとめて状態管理されていた

●Reactのアプリケーション性能を高める、状態管理をより分かりやすくするためには Server dataの取り扱いとReact app stateの取り扱いを分けて考える必要がある
React app stateは、従来のReduxやuseContextを使う
Server dataは、キャッシュ機構を活用することができるreact-queryを使用する
キャッシュには他のコンポーネントから自由にアクセスすることができるため、
ReduxやuseContextを使わずサーバーから取得したデータは
react-queryのキャッシュ機構を使って他のコンポーネントから自由にアクセスできる

キャッシュのメカニズムをうまく活用することで
・他のコンポーネントからアクセス可能
・Fetch回数の最適化

●Redux(useContext + useState)
コンポーネントAからfetchでアクセスし取得したデータを
dispatchを使用してstoreに保存
こうすることでコンポーネントBやコンポーネントCから
取得したデータに自由にアクセスできるようになっていた

●React-query
コンポーネントAからREST APIへuseQueryを使用してアクセスする
データを新規作成・更新、削除したりする場合は、useMutationを使用する
取得したデータは自動的にキャッシュに保存してくれる
キャッシュに保存されたデータにコンポーネントBやコンポーネントCなど
他のコンポーネントから自由にアクセスが可能になる
(ReduxやuseContextなどと同じようにグローバルに管理できる)

利点

1、Fetch回数の最適化
従来:
あるコンポーネントにuseEffectを導入
useEffectの中でREST APIにfetchするための処理を記述
例えば、第二引数を空の[ ]とすることで
コンポーネントがマウントされる度にfetchが実行されサーバから最新のデータを取得
別のコンポーネントへページを遷移して戻ってきたとき
再度マウントされるためfetchが毎回実行される

useQuery:
あるコンポーネントにuseQueryを導入
そのコンポーネントがマウントされた時にREST APIにアクセスし
取得したデータをキャッシュの機構に保存
react-queryではstaleTimeというパラメータを設定することができる
これによりfetchの回数を調整することができる
例えばstaleTimeを10秒(staleTime: 10000[ms])に設定した場合、
マウントしてから10秒間は、保存したキャッシュを最新のもの(freshなcache)と見なすようになる
10秒後にキャッシュの内容をstale(古いcache)と見なすようになる
そのため、今回の場合だと10秒間に再度マウントしたとしても
useQueryを実行したとしてもサーバへのfetchを行わないようにすることができる
また、取得するデータのエンドポイント毎にstaleTimeを設定することができる
サーバのデータがほとんど変わらないデータの場合、毎回fetchをするのではなく、
staleTimeを長めにとっておくことで無駄なfetchをなくすことが出来る

2、Better UX by Stale While Revalidation
従来:
あるコンポーネントにuseEffectを導入
マウント時にfetchの処理が実行されREST APIからデータを取得
非同期通信のため、データを取得するまでに少し時間がかかる
データ取得できるまでは何も表示されない
コンポーネントがマウントされる度にloadingのstateがユーザに見えてしまう形になる

useQuery:
あるコンポーネントにuseQueryを導入
初回はキャッシュが存在しないのでREST APIからデータを取得するため
従来と同じようにloadingの画面が表示される
2回目以降マウントされた際は既にキャッシュに何らかの値が保存されているため
キャッシュからデータをすぐに返してくれる
キャッシュのデータが古い可能性もあるがとりあえずキャッシュにあるデータを返してくれる
返している間に最新のデータを評価していく形になる
データが取得でき次第、最新のデータでstale dataを書き換えてくれる

3、コード量を少なくして処理をシンプルにすることが出来る
従来:
・fetch処理
・通信状況処理(データを取得できたか通信が終わったのか、エラーが発生したのかなど)
・グローバルState化(dispatch、useContextを使うなど)

import { useState, useEffect } from "react";
import { useStateContext } from "../context/StateProvider";
import axios from "axios";

export const useClassicFetch = () => {
  const { tasks, setTasks } = useStateContext();
  const { isLoading, setLoading } = useState(false);
  const { isError, setError } = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setError(false);
      setLoading(false);
      try {
        const res = await axios('API');
        setTasks(res.data);
      } catch (error) {
        setError(true);
      }
      setLoading(false);
    };
    fetchData();
  }, [setTasks]);

  return { tasks, isLoading, isError };
};

取得したデータをuseContextを使って
グローバル化する必要があるためProviderの設定が必要

import { useContext, useState, createContext } from "react";
import { Tasks } from "../types/types";

interface StateContextType {
  tasks: Task[] | null;
  dark: boolean;
  setTasks: React.Dispatch<React.SetStateAction<Task[] | null>>;
  setDark: React.Dispatch<React.SetStateAction<boolean>>;
}

const StateContext = createContext({} as StateContextType);

export const StateProvider: React.FC = ({ children }) => {
  const [ tasks, setTasks ] = useState<Task[] | null>(null);
  const [ dark, setDark ] = useState(false);

  return (
    <StateContext.Provider value={{ tasks, setTasks, dark, setDark }}>
      {children}
    </StateContext.Provider>
  )
}
export const useStateContext = (): StateContextType => useContext(StateContext)

useQuery:
下記で上記のようなことと同じようなことが出来る

const { isLoading, error, data } = useQuery('repoDate', () =>
  fetch('API').then(res =>
    res.json()
  )
)

Next.jsを使用する場合は、SWR(Vercel)を採用する方が良いかもしれない。
APOLLOクライアントは標準でキャッシュ機構が備わっている。

カスタムフック

カスタムフックとは

・hooksの各機能を使用
コンポーネントからロジックを分離
・使い回し、テスト容易、見通しが良くなる
・自由に作成できる

カスタムフックなしで実装

$ mkdir ./src/components
$ mkdir -p ./src/types/api
$ touch ./src/components/UserCard.tsx
$ touch ./scr/types/userProfile.ts
$ touch ./scr/types/api/user.ts
export type User = {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
};
export type UserProfile = {
  id: number;
  name: string;
  email: string;
  address: string;
};
import { VFC } from "react";
import { UserProfile } from "../types/userProfile";

type Props = {
  user: UserProfile;
};

export const UserCard: VFC<Props> = (props) => {
  const { user } = props;

  const style = {
    border: "solid 1px #ccc",
    borderRadius: "8px",
    padding: "0 16px",
    margin: "8px"
  };

  return (
    <div style={style}>
      <dl>
        <dt>名前</dt>
        <dd>{user.name}</dd>
        <dt>メール</dt>
        <dd>{user.email}</dd>
        <dt>住所</dt>
        <dd>{user.address}</dd>
      </dl>
    </div>
  );
};

・データ取得
・ローディング
・エラー

import "./styles.css";
import { UserCard } from "./components/UserCard";
import axios from "axios";
import { User } from "./types/api/user";
import { useState } from "react";
import { UserProfile } from "./types/userProfile";

export default function App() {
  const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const onClickFetchUser = () => {
    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(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  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>
  );
}

コンポーネントの肥大化。

カスタムフックを使用して実装

$ mkdir ./src/hooks
$ touch ./src/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<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(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  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>
  );
}

TypeScript 復習

基本的な型

$ npx create-react-app <プロジェクト名> --template typescript
/* eslint-disable @typescript-eslint/no-unused-vars */

/** TypeScriptの基本の型 */

// boolean
let bool: boolean = true;

// number
let num: number = 0;

// string
let str: string = "A";

// Array
let arr: Array<number> = [0, 1, 2];
let arr1: Array<string> = ["A", "B", "C"];
let arr2: number[] = [0, 1, 2];

// tuple
let tuple: [number, string] = [0, "A"];

// any
let any1: any = false;

// void 何も返却値がないことを表す
const funcA = () => {
  const test = "TEST";
};

// null
let null1: null = null;

// undefined
let undefined1: undefined = undefined;

// object
let obj1: object = {};
let obj2: {} = {};
let obj3: { id: number; name: string } = { id: 0, name: "AAA" };

引数の型指定

export const Practice1 = () => {
  const calcTotalFee = (num: number) => {
    const total = num * 1.1;
    console.log(total);
  };
  const onClickPractice = () => calcTotalFee(1000);

  return (
    <div>
      <p>練習問題:引数の型指定</p>
      <button onClick={onClickPractice}>練習問題1を実行</button>
    </div>
  );
};

引数の戻り値の型指定

export const Practice2 = () => {
  const calcTotalFee = (num: number): number => {
    const total = num * 1.1;
    return total;
  };
  const onClickPractice = () => calcTotalFee(1000);

  return (
    <div>
      <p>練習問題:引数の戻り値の型指定</p>
      <button onClick={onClickPractice}>練習問題2を実行</button>
    </div>
  );
};

変数の型指定

export const Practice3 = () => {
  const calcTotalFee = (num: number): number => {
    const total = num * 1.1;
    return total;
  };
  const onClickPractice = () => {
    let total: number = 0;
    total = calcTotalFee(1000);
    console.log(total);
  };

  return (
    <div>
      <p>練習問題:変数の型指定</p>
      <button onClick={onClickPractice}>練習問題3を実行</button>
    </div>
  );
};

型指定しない場合

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";

export default function App() {
  const [todos, setTodos] = useState<any>([]);

  const onClickFetchData = () => {
    axios.get("https://jsonplaceholder.typicode.com/todos").then((res) => {
      setTodos(res.data);
    });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo title={todo.title} userid={todo.userid} />
      ))}
    </div>
  );
}
export const Todo = (props) => {
  const { title, userid } = props;

  return <p>{`${title}(ユーザ:${userid})`}</p>;
};

エラーに気づけず意図した通りにデータを取得できない

型指定する場合

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";

type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export default function App() {
  // 配列の型指定 Array<>
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo title={todo.title} userid={todo.userId} />
      ))}
    </div>
  );
}

propsの型指定

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";

type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export default function App() {
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          title={todo.title}
          userId={todo.userId}
          completed={todo.completed}
        />
      ))}
    </div>
  );
}
type TodoType = {
  userId: number;
  title: string;
  // 必須でない場合は?をつける
  completed?: boolean;
};

export const Todo = (props: TodoType) => {
  const { title, userId, completed = false } = props;
  const completeMark = completed ? "[完]" : "[未]";

  return <p>{`${completeMark} ${title}(ユーザ:${userId})`}</p>;
};

型をまとめる

$ mkdir ./src/types
$ touch ./src/types/todo.ts

共通して使用する型を定義

export type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

Pickで使用したい型のみを指定する

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

export const Todo = (
  props: Pick<TodoType, "userId" | "title" | "completed">
) => {};

もしくは、Omitで必要ない型を指定する

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

export const Todo = (props: Omit<TodoType, "id">) => {};

定義した型はimportで読み込む

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";
import { TodoType } from "./types/todo";

export default function App() {
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          title={todo.title}
          userId={todo.userId}
          completed={todo.completed}
        />
      ))}
    </div>
  );
}

関数コンポーネントの型定義

$ touch ./src/Text.tsx

Functional Component(FC)

import { FC } from "react";

type Props = {
  color: string;
  fontSize: string;
};

export const Text: FC<Props> = (props) => {
  const { color, fontSize } = props;

  return <p style={{ color, fontSize }}>テキストです</p>;
};

FCは暗黙的にchildrenを受け取るため、reactのバージョン18まではVFCを使用する
VFCは暗黙的にchildrenを含まないFunctional Componentの型指定

import { VFC } from "react";

type Props = {
  color: string;
  fontSize: string;
};

export const Text: VFC<Props> = (props) => {
  const { color, fontSize } = props;

  return <p style={{ color, fontSize }}>テキストです</p>;
};
import { VFC } from "react";
import { TodoType } from "./types/todo";

export const Todo: VFC<Omit<TodoType, "id">> = (props) => {
  const { title, userId, completed = false } = props;
  const completeMark = completed ? "[完]" : "[未]";

  return <p>{`${completeMark} ${title}(ユーザ:${userId})`}</p>;
};
import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";
import { TodoType } from "./types/todo";
import { Text } from "./Text";

export default function App() {
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <Text color="red" fontSize="18px" />
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          title={todo.title}
          userId={todo.userId}
          completed={todo.completed}
        />
      ))}
    </div>
  );
}

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

$ touch ./src/types/user.ts
$ touch ./src/UserProfile.tsx
export type User = {
  name: string;
  hobbies?: Array<string>;
};
import { VFC } from "react";
import { User } from "./types/user";

type Props = {
  user: User;
};

export const UserProfile: VFC<Props> = (props) => {
  const { user } = props;

  return (
    <dl>
      <dt>名前</dt>
      <dd>{user.name}</dd>
      <dt>趣味</dt>
      // オプショナルチェイニング hobbiesに値が入っていなくてもエラーを吐かない
      <dd>{user.hobbies?.join(" / ")}</dd>
    </dl>
  );
};
import "./styles.css";
import { UserProfile } from "./UserProfile";
import { User } from "./types/user";

const user: User = {
  name: "Bob",
  hobbies: ["映画", "ゲーム"]
};

export default function App() {
  return (
    <div className="App">
      <UserProfile user={user} />
    </div>
  );
}

グローバルなstate管理

グローバルなstate管理

グローバルなstate管理あり・なしで管理者の時は編集リンクを表示させ、
一般ユーザの時は編集リンクを表示させないことを実装する

ツラい実装

まずはグローバルなstate管理なしで管理者の時は編集リンクを表示させ、
一般ユーザの時は編集リンクを表示させないことを実装する

管理者ユーザと一般ユーザ用のボタンを用意する

import { useHistory } from "react-router-dom";
import styled from "styled-components";
import { SecondaryButton } from "../atoms/button/SecondaryButton";

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

const onClickAdmin = () => history.push({ pathname: "/users", state: { isAdmin: true } }); const onClickGeneral = () => history.push({ pathname: "/users", state: { isAdmin: false } });

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

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

SecondaryButtonはonClickを受け取れるようになっている

import styled from "styled-components";
import { BaseButton } from "./BaseButton";

export const SecondaryButton = (props) => { const { children, onClick } = props;

return <SButton onClick={onClick}>{children}</SButton>; };

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

ページ遷移時のstateをuseLocationで取得

import { useLocation } from "react-router-dom";
import styled from "styled-components";
import { SearchInput } from "../molecules/SearchInput";
import { UserCard } from "../organisms/user/UserCard";

const users = [...Array(10).keys()].map((val) => { return { id: val, name: Bob$<span class="synIdentifier">{</span>val<span class="synIdentifier">}</span>, image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; });

export const Users = () => { const { state } = useLocation(); const isAdmin = state ? state.isAdmin : false;

return ( <SContainer> <h2>ユーザー一覧</h2> <SearchInput /> <SUserArea> {users.map((obj) => ( <UserCard key={obj.id} user={obj} isAdmin={isAdmin} /> ))} </SUserArea> </SContainer> ); };

const SContainer = styled.div text-align: center; flex-direction: column; align-items: center; padding: 24px; ;

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

渡されたisAdminを受け取る

import styled from "styled-components";
import { Card } from "../../atoms/card/Card";
import { UserIconWithName } from "../../molecules/user/UserIconWithName";

export const UserCard = (props) => { const { user, isAdmin } = props;

return ( <Card> <UserIconWithName image={user.image} name={user.name} isAdmin={isAdmin} /> <SDL> <dt>メール</dt> <dd>{user.email}</dd> <dt>TEL</dt> <dd>{user.phone}</dd> <dt>会社名</dt> <dd>{user.company.name}</dd> <dt>WEB</dt> <dd>{user.website}</dd> </SDL> </Card> ); };

const SDL = styled.dl text-align: left; margin-bottom: 0px; dt <span class="synIdentifier">{</span> <span class="synStatement">float</span>: left; <span class="synIdentifier">}</span> dd <span class="synIdentifier">{</span> padding-left: 32px; padding-bottom: 8px; overflow-wrap: <span class="synStatement">break</span>-word; <span class="synIdentifier">}</span> ;

渡されたisAdminを受け取る

import styled from "styled-components";

export const UserIconWithName = (props) => { const { image, name, isAdmin } = props; return ( <SContainer> <SImg height={160} width={160} src={image} alt={name} /> <SName>{name}</SName> {isAdmin && <SEdit>編集</SEdit>} </SContainer> ); };

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

const SImg = 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; ;

これがもっと規模が大きくなるとツラい。

これをグローバルなstate管理で解決していく。

Context

$ mkdir -p src/providers

$ touch src/providers/UserProvider.jsx

コンテキストのコンポーネントを作成

import { createContext, useState } from "react";

// 新しいコンテキストの作成 export const UserContext = createContext({});

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

const [userInfo, setUserInfo] = useState(null);

return ( // valueというグローバルなstateを渡す <UserContext.Provider value={{ userInfo, setUserInfo }}> {children} </UserContext.Provider> ); };

Providerは要素を囲む必要がある

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

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

コンテキストの値を参照していくにはuseContextを使用する

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

export const UserIconWithName = (props) => { const { image, name } = props; const { userInfo } = useContext(UserContext); const isAdmin = userInfo ? userInfo.isAdmin : false;

return ( <SContainer> <SImg height={160} width={160} src={image} alt="プロフィール写真" /> <SName>{name}</SName> {isAdmin && <SEdit>編集</SEdit>} </SContainer> ); };

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

const SImg = 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 { useContext } from "react";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
import { UserContext } from "../../providers/UserProvider";
import { SecondaryButton } from "../atoms/button/SecondaryButton";

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

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; ;

レンダリングの最適化(Context)

作成したプロバイダー(UserProvider)に設定したstate(userInfo, setUserInfo)の値が更新された場合、
その値を使用しているコンポーネント全てが再レンダリングされる

memoを使用してpropsに変更がない場合は再レンダリングしないという風に
コンポーネントを設定する必要がある

import { memo } from "react";

コンテキストを使う場合は、更新時にどのコンポーネントが再レンダリングされるか意識する

Atomic Design

Atomic Designとは

・Brad Frost氏が考案したデザインシステム
・画面要素を5段階に分け、組み合わせることでUIを実現
コンポーネント化された要素が画面を構成しているという考え方
・React、Vue用というわけではない
・モダンJavaScriptと相性がいい

5段階のコンポーネント

ATOMS → MOLECULES → ORGANISMS → TEMPLATES → PAGES
原子 → 分子 → 有機体 → テンプレート → ページ

ATOMS

Atoms 原子
最も小さくそれ以上分解できない要素

・ボタン
・ツイート入力テキストボックス
・アイコン 等

MOLECULES

Molecules 分子
Atomの組み合わせで意味を持つデザインパターン

・アイコン + メニュー名
プロフィール画像 + テキストボックス
・アイコンセット 等

ORGANISMS

Organisms 有機
AtomやMoleculeの組み合わせで構成される単体である程度の意味を持つ要素群

・ツイート入力エリア
・サイドメニュー
・1つのツイートエリア 等

TEMPLATES

Templates テンプレート
ページのレイアウトのみを表現する要素(実際のデータは持たない)

・サイドメニュー
・ツイートエリア
・トピックエリア等のレイアウト情報 等

PAGES

Pages ページ
最終的に表示される1画面

・ページ遷移毎に表示される各画面

Atom 作成

$ mkdir -p src/components/atoms/button
$ touch src/components/atoms/button/BaseButton.jsx
$ touch src/components/atoms/button/PrimaryButton.jsx
$ touch src/components/atoms/button/SecondaryButton.jsx

./components/atoms/button/BaseButton.jsx

import styled from "styled-components";

export const BaseButton = styled.button`
  color: #fff;
  padding: 6px 24px;
  border: none;
  border-radius: 9999px;
  outline: none;
  &:hover {
    cursor: pointer;
    opacity: 0.8;
  }
`;

./components/atoms/button/PrimaryButton.jsx

import styled from "styled-components";
import { BaseButton } from "./BaseButton";

export const PrimaryButton = (props) => {
  const { children } = props;

  return <SButton>{children}</SButton>;
};

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

./components/atoms/button/SecondaryButton.jsx

import styled from "styled-components";
import { BaseButton } from "./BaseButton";

export const SecondaryButton = (props) => {
  const { children } = props;

  return <SButton>{children}</SButton>;
};

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

Molecules 作成

$ mkdir -p src/components/molecules
$ mkdir -p src/components/atoms/input
$ touch src/components/molecules/SearchInput.jsx
$ touch src/components/atoms/input/Input.jsx

./components/atoms/input/Input.jsx

import styled from "styled-components";

export const Input = (props) => {
  const { placeholder = "" } = props;
  return <SInput type="text" placeholder={placeholder} />;
};

const SInput = styled.input`
  padding: 8px 16px;
  border: solid #ddd 1px;
  border-radius: 9999px;
  outline: none;
`;

./components/molecules/SearchInput.jsx

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

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

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

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

Organisms 作成

$ mkdir -p src/components/organisms/user
$ mkdir -p src/components/atoms/card
$ mkdir -p src/components/molecules/user
$ touch src/components/organisms/user/UserCard.jsx
$ touch src/components/atoms/card/Card.jsx
$ touch src/components/molecules/user/UserIconWithName.jsx

./components/atoms/card/Card.jsx

import styled from "styled-components";

export const Card = (props) => {
  const { children } = props;

  return <SCard>{children}</SCard>;
};

const SCard = styled.div`
  background-color: #fff;
  box-shadow: #ddd 0px 0px 4px 2px;
  border-radius: 8px;
  padding: 16px;
`;

./components/organisms/user/UserCard.jsx

import styled from "styled-components";
import { Card } from "../../atoms/card/Card";
import { UserIconWithName } from "../../molecules/user/UserIconWithName";

export const UserCard = (props) => {
  const { user } = props;

  return (
    <Card>
      <UserIconWithName image={user.image} name={user.name} />
      <SDL>
        <dt>メール</dt>
        <dd>{user.email}</dd>
        <dt>TEL</dt>
        <dd>{user.phone}</dd>
        <dt>会社名</dt>
        <dd>{user.company.name}</dd>
        <dt>WEB</dt>
        <dd>{user.website}</dd>
      </SDL>
    </Card>
  );
};

const SDL = styled.dl`
  text-align: left;
  margin-bottom: 0px;
  dt {
    float: left;
  }
  dd {
    padding-left: 32px;
    padding-bottom: 8px;
    overflow-wrap: break-word;
  }
`;

/components/molecules/user/UserIconWithName.jsx

import styled from "styled-components";

export const UserIconWithName = (props) => {
  const { image, name } = props;
  return (
    <SContainer>
      <SImg height={160} width={160} src={image} alt={name} />
      <SName>{name}</SName>
    </SContainer>
  );
};

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

const SImg = styled.img`
  border-radius: 50%;
`;
const SName = styled.p`
  font-size: 18px;
  font-weight: bold;
  margin: 0;
  color: #40514e;
`;

./App.jsx

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 "./styles.css";

export default function App() {
  const user = {
    name: "Bob",
    image: "https://source.unsplash.com/2l0CWTpcChI",
    email: "12345@example.com",
    phone: "090-1111-2222",
    company: {
      name: "テスト株式会社"
    },
    website: "https://google.com"
  };

  return (
    <div className="App">
      <PrimaryButton>テスト</PrimaryButton>
      <SecondaryButton>検索</SecondaryButton>
      <br />
      <SearchInput />
      <UserCard user={user} />
    </div>
  );
}

Template 作成

$ mkdir -p src/components/templates
$ mkdir -p src/components/atoms/layout
$ touch src/components/templates/DefaultLayout.jsx
$ touch src/components/templates/HeaderOnly.jsx
$ touch src/components/atoms/layout/Header.jsx
$ touch src/components/atoms/layout/Footer.jsx

./components/templates/HeaderOnly.jsx

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

export const Header = () => {
  return (
    <SHeader>
      <SLink to="/">HOME</SLink>
      <SLink to="/users">USERS</SLink>
    </SHeader>
  );
};

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

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

./components/templates/DefaultLayout.jsx

import { Footer } from "../atoms/layout/Footer";
import { Header } from "../atoms/layout/Header";

export const DefaultLayout = (props) => {
  const { children } = props;
  return (
    <>
      <Header />
      {children}
      <Footer />
    </>
  );
};

/components/atoms/layout/Header.jsx

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

export const Header = () => {
  return (
    <SHeader>
      <SLink to="/">HOME</SLink>
      <SLink to="/users">USERS</SLink>
    </SHeader>
  );
};

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

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

./components/atoms/layout/Footer.jsx

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

export const Footer = () => {
  return <SFooter>&copy; 2021 test Inc.</SFooter>;
};

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

./App.jsx

import { BrowserRouter } from "react-router-dom";
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 { HeaderOnly } from "./components/templates/HeaderOnly";
import "./styles.css";

export default function App() {
  const user = {
    name: "Bob",
    image: "https://source.unsplash.com/2l0CWTpcChI",
    email: "12345@example.com",
    phone: "090-1111-2222",
    company: {
      name: "テスト株式会社"
    },
    website: "https://google.com"
  };

  return (
    <BrowserRouter>
      <DefaultLayout>
        <PrimaryButton>テスト</PrimaryButton>
        <SecondaryButton>検索</SecondaryButton>
        <br />
        <SearchInput />
        <UserCard user={user} />
      </DefaultLayout>
    </BrowserRouter>
  );
}

Pages 作成

$ mkdir -p src/components/pages
$ mkdir -p src/router
$ touch src/components/pages/Top.jsx
$ touch src/components/page/Users.jsx
$ touch src/components/atoms/layout/Header.jsx
$ touch src/components/atoms/layout/Footer.jsx
$ touch src/router/Router.jsx

./components/pages/Top.jsx

import styled from "styled-components";

export const Top = () => {
  return (
    <SContainer>
      <h2>TOPページです</h2>
    </SContainer>
  );
};

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

./components/page/Users.jsx

import styled from "styled-components";
import { SearchInput } from "../molecules/SearchInput";
import { UserCard } from "../organisms/user/UserCard";

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

export const Users = () => {
  return (
    <SContainer>
      <h2>ユーザー一覧です</h2>
      <SearchInput />
      <SUserArea>
        {users.map((user) => (
          <UserCard key={user.id} user={user} />
        ))}
      </SUserArea>
    </SContainer>
  );
};

const SContainer = styled.div`
  text-align: center;
  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;
`;

./components/atoms/layout/Header.jsx

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

export const Header = () => {
  return (
    <SHeader>
      <SLink to="/">HOME</SLink>
      <SLink to="/users">USERS</SLink>
    </SHeader>
  );
};

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

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

./components/atoms/layout/Footer.jsx

import styled from "styled-components";

export const Footer = () => {
  return <SFooter>&copy; 2021 test Inc.</SFooter>;
};

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

./router/Router.jsx

import { BrowserRouter, Switch, Route } from "react-router-dom";
import { Top } from "../components/pages/Top";
import { Users } from "../components/pages/Users";
import { DefaultLayout } from "../components/templates/DefaultLayout";
import { HeaderOnly } from "../components/templates/HeaderOnly";

export const Router = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/">
          <DefaultLayout>
            <Top />
          </DefaultLayout>
        </Route>
        <Route path="/users">
          <HeaderOnly>
            <Users />
          </HeaderOnly>
        </Route>
      </Switch>
    </BrowserRouter>
  );
};

./App.jsx

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

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

React Router

ページ遷移

// BrowserRouter このコンポーネントで囲った配下でルーティングを有効にする
import { BrowserRouter, Link, Switch, Route } from "react-router-dom";

import { Home } from "./Home";
import { Page1 } from "./Page1";
import { Page2 } from "./Page2";
import "./styles.css";

export default function App() {
  return (
    <BrowserRouter>
      <Link to="/">Home</Link>
      <br />
      <Link to="/page1">Page1</Link>
      <br />
      <Link to="/page2">Page2</Link>
      <br />
      <div className="App"></div>
      {/* Switch どのパスの時にどのコンポーネントを表示するか設定する */}
      <Switch>
        {/* Route Routeにマッチしたパス・画面を表示する */}
        {/* exact 完全一致の場合 */}
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/page1">
          <Page1 />
        </Route>
        <Route path="/page2">
          <Page2 />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

ネストされたページ遷移

// BrowserRouter このコンポーネントで囲った配下でルーティングを有効にする
import { BrowserRouter, Link, Switch, Route } from "react-router-dom";

import { Home } from "./Home";
import { Page1 } from "./Page1";
import { Page1DetailA } from "./Page1DetailA";
import { Page1DetailB } from "./Page1DetailB";
import { Page2 } from "./Page2";
import "./styles.css";

export default function App() {
  return (
    <BrowserRouter>
      <Link to="/">Home</Link>
      <br />
      <Link to="/page1">Page1</Link>
      <br />
      <Link to="/page2">Page2</Link>
      <br />
      <div className="App"></div>
      {/* Switch どのパスの時にどのコンポーネントを表示するか設定する */}
      <Switch>
        {/* Route Routeにマッチしたパス・画面を表示する */}
        {/* exact 完全一致の場合 */}
        <Route exact path="/">
          <Home />
        </Route>
        <Route
          path="/page1"
          // render関数はpropsをデフォルトで受け取っている
          // console.log(props)で中身を確認
          // matchのurlを使用する
          render={({ match: { url }}) => (
            <Switch>
              <Route exact path={url}>
                <Page1 />
              </Route>
              <Route path={`${url}/detailA`}>
                <Page1DetailA />
              </Route>
              <Route path={`${url}/detailB`}>
                <Page1DetailB />
              </Route>
            </Switch>
          )}
        ></Route>
        <Route path="/page2">
          <Page2 />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

コンポーネントへ切り出し

// BrowserRouter このコンポーネントで囲った配下でルーティングを有効にする
import { BrowserRouter, Link } from "react-router-dom";
import { Router } from "./router/Router";
import "./styles.css";

export default function App() {
  return (
    <BrowserRouter>
      <Link to="/">Home</Link>
      <br />
      <Link to="/page1">Page1</Link>
      <br />
      <Link to="/page2">Page2</Link>
      <br />
      <div className="App"></div>
      <Router />
    </BrowserRouter>
  );
}
import { Switch, Route } from "react-router-dom";
import { Home } from "../Home";
import { Page2 } from "../Page2";
import { page1Routes } from "./Page1Routes";

export const Router = () => {
  return (
    <Switch>
      {/* Route Routeにマッチしたパス・画面を表示する */}
      {/* exact 完全一致の場合 */}
      <Route exact path="/">
        <Home />
      </Route>
      <Route
        path="/page1"
        render={({ match: { url } }) => (
          <Switch>
            {page1Routes.map((route) => (
              <Route
                key={route.path}
                exact={route.exact}
                path={`${url}${route.path}`}
              >
                {route.children}
              </Route>
            ))}
          </Switch>
        )}
      ></Route>
      <Route path="/page2">
        <Page2 />
      </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 />
  }
];

URLパラメータ

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

export const page2Routes = [
  {
    path: "/",
    exact: true,
    children: <Page2 />
  },
  {
    path: "/:id",
    exact: false,
    children: <UrlParameter />
  }
];
import { Switch, Route } from "react-router-dom";
import { Home } from "../Home";
import { page1Routes } from "./Page1Routes";
import { page2Routes } from "./Page2Routes";

export const Router = () => {
  return (
    <Switch>
      {/* Route Routeにマッチしたパス・画面を表示する */}
      {/* exact 完全一致の場合 */}
      <Route exact path="/">
        <Home />
      </Route>
      <Route
        path="/page1"
        render={({ match: { url } }) => (
          <Switch>
            {page1Routes.map((route) => (
              <Route
                key={route.path}
                exact={route.exact}
                path={`${url}${route.path}`}
              >
                {route.children}
              </Route>
            ))}
          </Switch>
        )}
      ></Route>
      <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>
        )}
      />
    </Switch>
  );
};
import { Link } from "react-router-dom";

export const Page2 = () => {
  return (
    <div>
      <h1>Page2ページです</h1>
      <Link to="/page2/999">URL Parameter</Link>
    </div>
  );
};
import { useParams } from "react-router-dom";

export const UrlParameter = () => {
  // useParams Page2Routes.jsxでidを設定しているためidを受け取れる
  const { id } = useParams();

  return (
    <div>
      <h1>UrlParameterページです</h1>
      <p>パラメータは {id} です</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 { useLocation, useParams } from "react-router-dom";

export const UrlParameter = () => {
  // useParams Page2Routes.jsxでidを設定しているためidを受け取れる
  const { id } = useParams();
  // useLocation searchにあるクエリパラメータを使用できる
  const { search } = useLocation();
  // URLSearchParams クエリパラメータを便利に扱えるメソッドを提供してくれる
  const query = new URLSearchParams(search);
  console.log(query);

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

stateを渡すページ遷移

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

export const Page1 = () => {
  // この100件の配列をDtailAページへ持っていく
  const arr = [...Array(100).keys()];
  console.log(arr);

  return (
    <div>
      <h1>Page1ページです</h1>
      {/* stateに対してarr変数を渡す */}
      <Link to={{ pathname: "page1/detailA", state: arr }}>DetailA</Link>
      <br />
      <Link to="/page1/detailB">DetailB</Link>
    </div>
  );
};
import { useLocation } from "react-router-dom";

export const Page1DetailA = () => {
  const { state } = useLocation();
  // stateが渡ってきているか確認
  console.log(state);

  return (
    <div>
      <h1>Page1DetailAページです</h1>
    </div>
  );
};

Linkを使わない遷移(js側で画面遷移)

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

export const Page1 = () => {
  const arr = [...Array(100).keys()];

  const history = useHistory();

  // pushメソッドで遷移先のURLを設定
  const onClickDetailA = () => history.push("page1/detailA");

  return (
    <div>
      <h1>Page1ページです</h1>
      <Link to={{ pathname: "page1/detailA", state: arr }}>DetailA</Link>
      <br />
      <Link to="/page1/detailB">DetailB</Link>
      <br />
      <button onClick={onClickDetailA}>DetailA</button>
    </div>
  );
};
import { useHistory, useLocation } from "react-router-dom";

export const Page1DetailA = () => {
  const { state } = useLocation();

  const history = useHistory();

  // goBackメソッドで遷移元へ戻る
  const onClickBack = () => history.goBack();

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

404ページ

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

export const Page404 = () => {
  return (
    <div>
      <h1>ページが見つかりません</h1>
      <Link to="/">TOPに戻る</Link>
    </div>
  );
};
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 exact path="/">
        <Home />
      </Route>
      <Route
        path="/page1"
        render={({ match: { url } }) => (
          <Switch>
            {page1Routes.map((route) => (
              <Route
                key={route.path}
                exact={route.exact}
                path={`${url}${route.path}`}
              >
                {route.children}
              </Route>
            ))}
          </Switch>
        )}
      ></Route>
      <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="*">
        <Page404 />
      </Route>
    </Switch>
  );
};