- useState
- サンプル画面
- サンプルのコンポーネントツリーイメージ図
- React.memo
- Props のバケツリレー
- サンプル画面
- Context を使う
- Provider Tower, Provider Hell
- サンプルイメージ
- Recoil 導入
- 参考
useState
state が更新されるとすべてのコンポーネントが再レンダリングされる
サンプル画面
サンプルのコンポーネントツリーイメージ図
App.tsx でのみ useStete を利用する
App.tsx
import { useState } from 'react';
import { Child1 } from './components/Chilid1';
import { Child4 } from './components/Chilid4';
const App = () => {
// App でのみ利用
const [num, setNum] = useState(0)
const onClickButton = () => {
setNum((prev) => prev + 1)
}
return (
<div>
<p>{num}</p>
<button onClick={onClickButton}>ボタン</button>
<Child1 />
<Child4 />
</div>
)
}
export default App
Child1.tsx
import { Child2 } from "./Chilid2"
import { Child3 } from "./Chilid3"
const style = {
height: "200px",
backgroundColor: "lightBlue",
margin: "8px"
}
export const Child1 = () => {
return (
<div style={style}>
<p>Child1</p>
<Child2 />
<Child3 />
</div>
)
}
Child2.tsx
const style = {
height: "50px",
backgroundColor: "lightGray",
margin: "8px"
}
export const Child2 = () => {
return (
<div style={style}>
<p>Child2</p>
</div>
)
}
Child3.tsx
const style = {
height: "50px",
backgroundColor: "lightGray",
margin: "8px"
}
export const Child3 = () => {
return (
<div style={style}>
<p>Child3</p>
</div>
)
}
Child4.tsx
const style = {
height: "200px",
backgroundColor: "lightBlue",
margin: "8px"
}
export const Child4 = () => {
return (
<div style={style}>
<p>Child4</p>
</div>
)
}
React Developer Tools
Highlight updates when components render. を ON にすると再レンダリングされたコンポーネントが視覚的に確認できる
ボタンをクリックすると全コンポーネントが再レンダリングされることが分かる
React.memo
Props に変更がない限り再レンダリングされない
// React.memo 追加
export const Child1 = memo(() => {
return (
<div style={style}>
<p>Child1</p>
<Child2 />
<Child3 />
</div>
)
})
memo を追加することにより Child1, Child2, Child3 コンポーネントが再レンダリングされなくなる
再レンダリングされるコンポーネントツリーのイメージ図
他に、子コンポーネントに Props として渡す関数は useCallback を使いメモ化する
変数設定のロジックが複雑だったり、膨大なループが実行される場合等の変数に useMemo を使いメモ化する
Props のバケツリレー
Props のバケツリレーを使うとどういったデメリットがあるか
- Props の値が変わるとコンポーネントが再レンダリングされる
- 該当コンポーネントが Props の値を利用していなくても state 更新時に再レンダリングされる
- 他の場所で再利用しにくいコンポーネントになる
サンプル画面
管理者の場合は「編集」ボタンをクリックできる
App.tsx
import { FC, useState } from 'react';
import { Card } from './components/Card';
import { Child1 } from './components/Chilid1';
import { Child4 } from './components/Chilid4';
const App: FC = () => {
const [num, setNum] = useState(0)
const onClickButton = () => setNum((prev) => prev + 1)
// 追加
const [isAdmin, setIsAdmin] = useState(false)
const onClickSwitch = () => setIsAdmin(!isAdmin)
return (
<>
<div>
<p>{num}</p>
<button onClick={onClickButton}>ボタン</button>
<Child1 />
<Child4 />
</div>
<div>
{ isAdmin ? <span>管理者です</span>: <span>管理者以外です</span>}
<button onClick={onClickSwitch}>切り替え</button>
<Card isAdmin={isAdmin} />
</div>
</>
)
}
export default App
Cart.tsx
import { FC, memo } from "react"
import { EditButton } from "./EditButton"
const style = {
width: "300px",
height: "200px",
margin: "8px",
borderRadius: "8px",
backgroundColor: "#e9dbd0",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
export const Card: FC<{isAdmin: boolean}> = memo(({isAdmin}) => {
return (
<div style={style}>
<p>山田太郎</p>
{/* isAdmin を受け取って渡しているだけ */}
<EditButton isAdmin={isAdmin} />
</div>
)
})
EditButton.tsx
import { FC, memo } from "react"
export const EditButton: FC<{isAdmin: boolean}> = memo(({isAdmin}) => {
return (
<div>
<button disabled={!isAdmin}>編集</button>
</div>
)
})
App, Card, EditButton すべてのコンポーネントが再レンダリングされる
Context を使う
Context を使うとどういった時に再レンダリングが起きるか
- useContext でその Context を参照しているコンポーネントが再レンダリングされる
※ 1つの Context に属性の異なるいろんな State を詰め込むのを避けるためには Provier をネストする必要あり(Provider Tower!Provider Hell!)
AdminFlgProvider.tsx
import { createContext, FC, ReactNode, useState } from "react";
interface AdminFlgInterface {
isAdmin: boolean,
setIsAdmin: (isAdmin: boolean) => void
}
export const AdminFlgContext = createContext({} as AdminFlgInterface)
export const AdminFlgProvider: FC<{ children: ReactNode }> = ({ children }) => {
// state を Provider で定義
const [isAdmin, setIsAdmin] = useState(false)
return (
<AdminFlgContext.Provider value={{ isAdmin, setIsAdmin }}>
{ children }
</AdminFlgContext.Provider>
)
}
App.tsx
import { FC, useContext, useState } from 'react';
import { AdminFlgContext } from './components/AdminFlgProvider';
import { Card } from './components/Card';
import { Child1 } from './components/Chilid1';
import { Child4 } from './components/Chilid4';
const App: FC = () => {
const [num, setNum] = useState(0)
const onClickButton = () => setNum((prev) => prev + 1)
// context から値を取得
const { isAdmin, setIsAdmin } = useContext(AdminFlgContext)
const onClickSwitch = () => setIsAdmin(!isAdmin)
return (
<>
<div>
<p>{num}</p>
<button onClick={onClickButton}>ボタン</button>
<Child1 />
<Child4 />
</div>
<div>
{ isAdmin ? <span>管理者です</span>: <span>管理者以外です</span>}
<button onClick={onClickSwitch}>切り替え</button>
<Card />
</div>
</>
)
}
export default App
Card.tsx
import { memo } from "react"
import { EditButton } from "./EditButton"
const style = {
width: "300px",
height: "200px",
margin: "8px",
borderRadius: "8px",
backgroundColor: "#e9dbd0",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
export const Card = memo(() => {
return (
<div style={style}>
<p>山田太郎</p>
{/* isAdmin を受け取って渡さなくなる */}
<EditButton />
</div>
)
})
EditButton.tsx
import { memo, useContext } from "react"
import { AdminFlgContext } from "./AdminFlgProvider"
export const EditButton = memo(() => {
// context から値を取得する
const isAdmin = useContext(AdminFlgContext)
return (
<div>
<button disabled={!isAdmin}>編集</button>
</div>
)
})
App, EditButton が再レンダリングされ、Card は再レンダリングされなくなる
Provider Tower, Provider Hell
UserNameProvider を追加してちょっとした Provider Towerをつくってみる(自分的には Provider タワーの方が好きな名称)
サンプルイメージ
index.tsx(UserNameProvider 追加)
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<AdminFlgProvider>
<UserNameProvider>
<App />
</UserNameProvider>
</AdminFlgProvider>
</React.StrictMode>
);
UserNameProvider.tsx(新規)
import { createContext, FC, ReactNode, useState } from "react";
interface UserNameInterface {
userName: string,
setUserName: (userName: string) => void
}
export const UserNameContext = createContext({} as UserNameInterface)
export const UserNameProvider: FC<{ children: ReactNode}> = ({ children }) => {
const [userName, setUserName] = useState("山田太郎")
return (
<UserNameContext.Provider value={{ userName, setUserName }}>
{ children }
</UserNameContext.Provider>
)
}
App.tsx
import React, { useCallback, useContext, useState } from 'react';
import { AdminFlgContext } from './components/AdminFlgProvider';
import { Card } from './components/Card';
import { Child1 } from './components/Chilid1';
import { Child4 } from './components/Chilid4';
import { UserNameContext } from './components/UserNameProvider';
const App = () => {
const [num, setNum] = useState(0)
const onClickButton = () => setNum((prev) => prev + 1)
const { isAdmin, setIsAdmin } = useContext(AdminFlgContext)
const onClickSwitch = useCallback(() => setIsAdmin(!isAdmin), [isAdmin, setIsAdmin])
// 追加
const { userName, setUserName } = useContext(UserNameContext)
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = useCallback((e) => setUserName(e.target.value), [setUserName])
return (
<>
<div>
<p>{num}</p>
<button onClick={onClickButton}>ボタン</button>
<Child1 />
<Child4 />
</div>
<div>
{ isAdmin ? <span>管理者です</span>: <span>管理者以外です</span>}
{/* 追加 */}
<button onClick={onClickSwitch}>切り替え</button>
<input type="text" value={userName} onChange={onChangeName} />
<Card />
</div>
</>
)
}
export default App
Card.tsx
import { memo, useContext } from "react"
import { EditButton } from "./EditButton"
import { UserNameContext } from "./UserNameProvider"
const style = {
width: "300px",
height: "200px",
margin: "8px",
borderRadius: "8px",
backgroundColor: "#e9dbd0",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
export const Card = memo(() => {
// 追加
const { userName } = useContext(UserNameContext)
return (
<div style={style}>
<p>{ userName }</p>
<EditButton />
</div>
)
})
EditButton.tsx(変更なし)
import { memo, useContext } from "react"
import { AdminFlgContext } from "./AdminFlgProvider"
export const EditButton = memo(() => {
const { isAdmin } = useContext(AdminFlgContext)
return (
<div>
<button disabled={!isAdmin}>編集</button>
</div>
)
})
名前変更時は App, Card コンポーネントが再レンダリングされる
Recoil 導入
2022年10月時点のバージョンは 0.7.6
インストール
$ yarn add recoil
使い方 Getting Started
index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot } from 'recoil';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
{/* <AdminFlgProvider> */}
{/* <UserNameProvider> */}
<RecoilRoot>
<App />
</RecoilRoot>
{/* </UserNameProvider> */}
{/* </AdminFlgProvider> */}
</React.StrictMode>
);
reportWebVitals();
useIsAdminSelect.tsx(AdminFlgProvier の代わり)
import { atom, useRecoilState } from "recoil"
export const isAdminState = atom({
key: "isAdminState",
default: false
})
useUserNameSelect.tsx(UserNameProvier の代わり)
import { atom, useRecoilState } from "recoil"
export const userNameState = atom({
key: "userName",
default: "山田太郎"
})
App.tsx
import React, { FC, useState } from 'react';
import { Card } from './components/Card';
import { Child1 } from './components/Chilid1';
import { Child4 } from './components/Chilid4';
import { isAdminState } from './components/hooks/useIsAdminSelect';
import { useRecoilState } from 'recoil';
import { userNameState } from './components/hooks/useUserNameSelect';
const App: FC = () => {
const [num, setNum] = useState(0)
const onClickButton = () => setNum((prev) => prev + 1)
// recoil に変更
const[ isAdmin, setIsAdmin ] = useRecoilState(isAdminState)
const onClickSwitch = useCallback(() => setIsAdmin(!isAdmin), [isAdmin, setIsAdmin])
// recoil に変更
const [ userName, setUserName ] = useRecoilState(userNameState)
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = (e) => setUserName(e.target.value)
// useSetRecoilState で Setter だけ取得も可能
// Setter だけ利用しているコンポーネントは再レンダリングされない
// const setUserName = useSetRecoilState(userNameState)
return (
<>
<div>
<p>{num}</p>
<button onClick={onClickButton}>ボタン</button>
<Child1 />
<Child4 />
</div>
<div>
{ isAdmin ? <span>管理者です</span>: <span>管理者以外です</span>}
<button onClick={onClickSwitch}>切り替え</button>
<input type="text" value={userName} onChange={onChangeName} />
<Card/>
</div>
</>
)
}
export default App
Cart.tsx
import { memo } from "react"
import { EditButton } from "./EditButton"
import { userNameState } from "./hooks/useUserNameSelect"
import { useRecoilState } from "recoil"
const style = {
width: "300px",
height: "200px",
margin: "8px",
borderRadius: "8px",
backgroundColor: "#e9dbd0",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
export const Card = memo(() => {
// recoil に変更
const [ userName ] = useRecoilState(userNameState)
return (
<div style={style}>
<p>{ userName }</p>
<EditButton />
</div>
)
})
EditButton.tsx
import { memo } from "react"
import { useRecoilState } from "recoil"
import { isAdminState } from "./hooks/useIsAdminSelect"
export const EditButton = memo(() => {
// recoil に変更
const [ isAdmin ] = useRecoilState(isAdminState)
return (
<div>
<button disabled={!isAdmin}>編集</button>
</div>
)
})
再レンダリングされるコンポーネントは Context 利用時と変わらない
参考
あとで試す用の参考