Closed55
Reactのメモ
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;
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でバケツリレーするっぽい。
確認事項
- グローバルに使用したい状態はどうやって扱うんだろう?ログイン, ダークモードなど?
userEffect
component再レンダリングの条件
- stateの変更
- 受け取っているpropsの変更
- 親コンポーネントの変更
無限レンダリングの例
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を使う
- useEffectの第二引数に[]を入れると初回のみレンダリング
- [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]);
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>
</>
);
};
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>
</>
);
});
よくある削除対応
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>
</>
);
};
レンダリング最適化
- memo関数を使う
- useCallback関数を使う
- useMemo関数を使う
レンダリングの最適化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()って何やってるんやろう?
レンダリングの最適化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>
);
};
レンダリングの最適化3 ... useMemo関数を使う
値の計算についてもレンダリングのたびに走ると困るものもある。
その場合はuseMemo()を使用する
// const temp = (() => {
// console.log("計算"); // レンダリングの都度計算が走る
// return 100 * 3;
// })();
const temp = useMemo(() => {
console.log("計算");
return 1 + 3;
}, []); // 初回一回で済む。
console.log(temp);
Styleの当て方
- inline style
- css module
- Styled Jsx
- Styled Component
- Emotion
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>
);
};
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;
}
}
Styleの当て方3 ... Styled Jsx
特徴
- styled jsxはnext.jsに組み込みになっている
- そのままだと疑似要素使えないので注意が必要
- 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>
</>
);
};
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>
);
};
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;
}
`;
ReactRouterの使い方
いったんver.5系で進めるので注意。
確認事項
ver.5 => ver.6 について確認
基本的な使い方
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>
);
}
ネストされたページ遷移
ルーティングの定義
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>
);
};
ルーターのリファクタリング
-
routes/Router.tsx
にルーター定義を移動 -
App.tsx
内では<Router />だけを配置 - ネストされたページは必要な反復している要素を配列化 ※1
router/Page1Routes.tsx
-
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>
);
};
パスパラメーターの取得
- 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>
);
};
クエリストリングの取得
-
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>
);
};
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>
);
};
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>
);
};
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>
);
};
Atomi Design
- Atoms ... それ以上分解できないもの。
- 例: ボタン、アイコン
- Molecuels ... Atomの組み合わせで意味をもつもの
- 例: アイコン + メニュー名, テキストボックス + ボタン
- Organisms ... AtomやMoleculeの組み合わせで構成される単体である程度の意味をもつ要素群
- ツイートエリア
- Templates ... ページのレイアウトのみを表現する要素
- データをもたない
- Pages ... 最終的に表示される1画面
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>;
};
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>
);
};
Organismsの実装
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>
);
}
コツ
- 意固地にならない。プロジェクトにより柔軟に対応
- 最初から分けない。まずは作ってリファクタリング
- 要素の関心を意識
Contextを用いたGlobalState管理
Provider, Stateの定義
-
providers/UserProvier.tsx
を作成 -
createContext()
でContextを作成 - ProviderComponentを作成(例:
UserProvider
) - Providerを使用するComponentをWrapする
Stateの使用
const { userInfo, setUserInfo } = useContext(UserContext);
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>
);
}
使用時
適当.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>
);
};
再レンダリングに注意
Contextで値を変更すると参照しているコンポーネントが全て再レンダリングされる。
影響範囲を調べてmemo化することを忘れないようにする。
Recoilを使用したグローバルステート管理
-
store/UserStaet.ts
を定義。(コンポーネント作らないので.ts
でOK) - 使用する
- 参照と更新する
- 参照だけする
- 更新だけする(再レンダリング予防)
定義
store/UserState.ts
import { atom } from "recoil";
export const userState = atom({
key: "userState",
default: { isAdmin: false }
});
使用する
参照と更新
適当な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);
Reactでよく使う型
- VFC, FC
- Pick, Omit
- ReactNode
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>;
};
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">
オプショナルチェイニング
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>
);
};
ライブラリの型定義
- index.d.tsがオブジェクトルートにあれば型定義を包含 ... axios
- @types/が必要 ... react-router-dom
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}
</>
);
});
# カスタムフック
- ただの関数
- hooksの各機能を使用
- コンポーネントからロジックを分離
- 使い回し、テスト容易、見通しよくなる
- use~と命名する
カスタムフックス
- フック内にロジックと状態を閉じ込めることができる。
- 使用する側のコードがかなりシンプルになる。
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>
);
}
ReactRouter v5 => v6
- Switch => Routes
- useHistory => useNavigate
参考
Switch => Routes
-
Switch
はRoute
で置き換え - 描画するコンポーネントは
element
で受ける - 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 />
</>
);
});
useHistory => useNavigate
適当.tsx
export const Header: FC = memo(() => {
const navigate = useNavigate();
const onClickHome = useCallback(() => navigate("/home"), [navigate]);
})
このスクラップは2022/10/07にクローズされました