React hooks & Recoil的速查表是为了给开发者提供快速查询React hooks和Recoil相关信息而创建的
首先
我会公开公司内部举办的React Recoil学习研讨会的内容。
如果我错了,请向我扔石头!
React hooks是什么?
-
- React Hooksは、React16.8.0以降で利用可能な関数コンポーネントのAPI
-
- コンポーネント間のロジックを再利用できる
-
- UIとロジックを分けることができる
- 主流となった 関数コンポーネント + hooks が対象
钩子 API 参考文档
目录
useState 基本フック コンポーネントに閉じた状態(local state)を定義
useReducer useStateの代替。値をセットするのではなく値をどういう操作をするかを定義
useEffect 基本フック コンポーネントのライフサイクルを定義
memo コンポーネントをメモ化(キャッシュ)し、不要なレンダリングを抑制
useMemo 関数の戻り値をメモ化(キャッシュ)し、不要な再実行を抑制
useCallback 関数をメモ化(キャッシュ)し、メモ化されたコンポーネントの不要なレンダリングを抑制
custom hook local stateのロジックをコンポーネントから分離し、テスト可能にした例
context Reactのcontext apiを利用したglobal stateの定義
context optimize Reactのcontext apiを利用したglobal stateのレンダリング最適化した例
recoil 状態管理ライブラリrecoilを利用したglobal stateの利用例
recoil optimize 状態管理ライブラリrecoilを利用したglobal stateのレンダリング最適化した例
recoil custom hook global stateのロジックをコンポーネントから分離し、テスト可能にした例
recoil selector recoil selectorを利用した計算値の取得と不要な再実行の抑制
根据目的制定的索引
-
- 基本hookについて
useState
useEffect
context
状態管理について
useState
context
recoil
レンダリング最適化について
memo
useMemo
useCallback
context optimize
recoil optimize
recoil selector
ロジック分離とテスト
useReducer
custom hook
recoil custom hook
使用状态
定义一个在基本挂钩组件中封闭状态(本地状态)。
/src/components/UseState.tsx的中文释义如下:
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const UseStateComponent: React.FC = () => {
const [count, setCount] = React.useState(0);
console.log('UseStateComponent render');
return (
<Box>
<Typography>Count: {count}</Typography>
<Button onClick={() => setCount(0)}>Reset</Button>
<Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
</Box>
);
};
export default UseStateComponent;
/src/App.tsx 的原文如下:
...
+ import UseState from './components/UseState';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/use-state" element={<UseState />} />
...
</Routes>
</BrowserRouter>
);
-
- useStateによりコンポーネントの状態が作られる
-
- 戻り値の値とそのセッターを利用して表示や状態の変更ができる
- 状態の変更を検知して再レンダリングされる
使用useReducer
useState的替代品。不設置值,而是定義對值進行什麼操作。
/src/components/UseReducer.tsx 的中文释义是什么?
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const reducer = (count: number, action: string)=> {
switch (action){
case 'increment':
return count + 1
case 'reset':
return 0
default:
return count
}
};
const UseReducerComponent: React.FC = () => {
const [count, dispatch] = React.useReducer(reducer, 0)
return (
<Box>
<Typography>Count: {count}</Typography>
<Button onClick={() => dispatch('reset')}>Reset</Button>
<Button onClick={() => dispatch('increment')}>+</Button>
</Box>
);
};
export default UseReducerComponent;
/src/App.tsx 的中文本地化改写如下:
/src/App.tsx
...
+ import UseReducer from './components/UseReducer';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/use-reducer" element={<UseReducer />} />
...
</Routes>
</BrowserRouter>
);
-
- useReducerによりコンポーネントの状態が作られる
-
- 戻り値の値とその変更関数を利用して表示や状態の変更ができる
-
- useStateとは違い 値をセットする のではなく 値にどういう操作をする かを定義する
-
- reducerが純粋な関数なのでロジックをコンポーネントから剥がしやすくテストしやすい
- ただし、useStateを使ったcustom hookを作成することで同様のメリットは得られる
使用useEffect
定义基本钩子组件的生命周期
/src/components/UseEffect.tsx的中文本地化翻译
import React from 'react';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const UseEffectComponent: React.FC = () => {
const navigate = useNavigate();
const [count, setCount] = React.useState(0);
console.log('UseEffectComponent render');
React.useEffect(() => {
console.log('UseEffectComponent mount');
return (() => {
console.log('UseEffectComponent unmount');
});
}, []);
React.useEffect(() => {
console.log('UseEffectComponent change count');
}, [count]);
return (
<Box>
<Typography>Count: {count}</Typography>
<Button onClick={() => setCount(0)}>Reset</Button>
<Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
<Button onClick={() => navigate('/use-state')}>go /use-state</Button>
</Box>
);
};
export default UseEffectComponent;
/src/App.tsx 的跨语言重构中
...
+ import UseEffect from './components/UseEffect';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/use-effect" element={<UseEffect />} />
...
</Routes>
</BrowserRouter>
);
-
- useEffectによりコンポーネントのライフサイクルを定義できる
-
- mountされた時、APIリクエストしてデータを取ってくるなどの前処理
-
- unmountされた時、websocketの接続を切るなどの後処理
- 第二引数に指定された値(状態でもpropsでも)が変更された時、データを再度取ってくるなどの変更処理
备忘录
对组件进行记忆化(缓存),抑制不必要的渲染。
/src/components/Memo.tsx的中文意思是什么?
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
interface CountComponentProps {
count: number;
}
const CountComponent: React.FC<CountComponentProps> = ({ count }) => {
console.log('CountComponent render');
return (
<Typography>Count: {count}</Typography>
);
};
const MemorizeCountComponent: React.FC<CountComponentProps> = React.memo(({ count }) => {
console.log('MemorizeCountComponent render');
return (
<Typography>Count: {count}</Typography>
);
});
const MemoComponent: React.FC = () => {
const [count1, setCount1] = React.useState(0);
const [count2, setCount2] = React.useState(0);
console.log('MemoComponent render');
return (
<Box>
<CountComponent count={count1} />
<Button onClick={() => setCount1(0)}>Reset</Button>
<Button onClick={() => setCount1(prevCount => prevCount + 1)}>+</Button>
<MemorizeCountComponent count={count2} />
<Button onClick={() => setCount2(0)}>Reset</Button>
<Button onClick={() => setCount2(prevCount => prevCount + 1)}>+</Button>
</Box>
);
};
export default MemoComponent;
/src/App.tsx 的中文本地化翻译为:精简版本为 /src/应用.tsx。
...
+ import Memo from './components/Memo';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/memo" element={<Memo />} />
...
</Routes>
</BrowserRouter>
);
-
- memoによりコンポーネントをメモ化(キャッシュ)し、不要なレンダリングを抑制できる
-
- メモ化されていないコンポーネントは親のレンダリング時、渡された値の変化にかかわらず再レンダリングされる
- メモ化されたコンポーネントは渡された値が変化しない限りレンダリングされない
使用useMemo
将函数的返回值进行记忆化(缓存),以抑制不必要的重新执行。
/src/components/UseMemo.tsx 的内容可以用汉语表达为:
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const UseMemoComponent: React.FC = () => {
const [count1, setCount1] = React.useState(0);
const [count2, setCount2] = React.useState(0);
const double = (value: number) => {
console.log('calculate double');
return value * 2;
};
const doubleCount1 = React.useMemo(() => double(count1), [count1]);
const doubleCount2 = double(count2);
return (
<Box>
<Typography>Count: {doubleCount1}</Typography>
<Button onClick={() => setCount1(0)}>Reset</Button>
<Button onClick={() => setCount1(prevCount => prevCount + 1)}>+</Button>
<Typography>Count: {doubleCount2}</Typography>
<Button onClick={() => setCount2(0)}>Reset</Button>
<Button onClick={() => setCount2(prevCount => prevCount + 1)}>+</Button>
</Box>
);
};
export default UseMemoComponent;
/src/App.tsx的中文本地化:
...
+ import UseMemo from './components/UseMemo';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/use-memo" element={<UseMemo />} />
...
</Routes>
</BrowserRouter>
);
-
- useMemoにより関数の戻り値をメモ化(キャッシュ)し、不要な再実行を抑制できる
-
- メモ化されていない関数の戻り値は親のレンダリング時、利用している値の変化にかかわらず再実行される
- メモ化された関数の戻り値は、利用している値が変化しない限り再実行されない
使用useCallback
对函数进行记忆化(缓存),以抑制记忆化组件的不必要渲染。
/src/components/UseCallback.tsx 的部分組件
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
interface LogButtonComponentProps {
onClick: () => void;
}
const LogButtonComponent: React.FC<LogButtonComponentProps> = ({ onClick }) => {
console.log('LogButtonComponent render');
return (
<Button onClick={onClick}>log</Button>
);
};
const MemorizeLogButtonComponent: React.FC<LogButtonComponentProps> = React.memo(({ onClick }) => {
console.log('MemorizeLogButtonComponent render');
return (
<Button onClick={onClick}>memorize log</Button>
);
});
const UseCallbackComponent: React.FC = () => {
const [count1, setCount1] = React.useState(0);
const [count2, setCount2] = React.useState(0);
const handleClick = React.useCallback(() => {
console.log(`count: ${count1}`);
}, [count1]);
return (
<Box>
<Typography>Count: {count1}</Typography>
<Button onClick={() => setCount1(prevCount => prevCount + 1)}>+</Button>
<LogButtonComponent onClick={handleClick} />
<MemorizeLogButtonComponent onClick={handleClick} />
<Typography>Count: {count2}</Typography>
<Button onClick={() => setCount2(prevCount => prevCount + 1)}>+</Button>
</Box>
);
};
export default UseCallbackComponent;
/src/App.tsx 的中文翻译可以是:
/src/App.tsx文件
...
+ import UseCallback from './components/UseCallback';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/use-callback" element={<UseCallback />} />
...
</Routes>
</BrowserRouter>
);
-
- useCallbackにより関数をメモ化(キャッシュ)し、メモ化されたコンポーネントの不要なレンダリングを抑制できる
-
- 基本的に関数はレンダリング時に再作成され、以前の関数と違う関数(等価ではない)を渡すことになるので、メモ化されているコンポーネントに渡しているかに関わらず再レンダリングされる
-
- useCallbackを通すことにより、前回と同じ(等価な)関数を作ることができる
-
- ただし、useCallbackで作成した関数でもメモ化されていないコンポーネントに渡す場合は再レンダリングされる
- useCallbackで作成した関数をメモ化されたコンポーネントに渡すことで再レンダリングを抑制できる
自定义挂钩
将本地状态的逻辑与组件分离,使其可进行测试的示例。
/src/components/CustomHookStore.tsx 的中文翻译如下:
/custom/hooks/CustomHookStore.tsx
import React from 'react';
export const useCounter = (initialCount = 0) => {
const [count, setCount] = React.useState(initialCount);
const increment = () => {
setCount((count) => count + 1);
};
const reset = () => {
setCount(0);
};
return { count, increment, reset };
}
/src/components/CustomHook.tsx
自定义钩子.tsx
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useCounter } from './CustomHookStore';
const CustomHookComponent: React.FC = () => {
const { count, increment, reset } = useCounter(0);
return (
<Box>
<Typography>Count: {count}</Typography>
<Button onClick={() => increment()}>+</Button>
<Button onClick={() => reset()}>Reset</Button>
</Box>
);
};
export default CustomHookComponent;
/src/App.tsx 的中文翻译如下:
/src/App.tsx
...
+ import CustomHook from './components/CustomHook';
const App: FC = () => (
<BrowserRouter>
<Routes>
...
+ <Route path="/custom-hook" element={<CustomHook />} />
...
</Routes>
</BrowserRouter>
);
- カスタムフックを定義することにより、ロジックをコンポーネントから分離できる
考试 shì)
/src/components/CustomHook.test.tsx 的中文翻译如下:
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import CustomHook from './CustomHook';
import * as CustomHookStoreModule from './CustomHookStore';
afterEach(() => cleanup());
describe('CustomHook', () => {
it('CustomHookが表示される', () => {
const { asFragment } = render(
<CustomHook />,
);
expect(asFragment()).toMatchSnapshot();
});
it('+ボタンが押されるとincrementが呼ばれる', () => {
const mockIncrement = jest.fn();
jest.spyOn(CustomHookStoreModule, 'useCounter').mockReturnValueOnce({
count: 1,
increment: mockIncrement,
reset: () => {},
});
render(
<CustomHook />,
);
const button = screen.getByRole('button', { name: '+' });
fireEvent.click(button);
expect(mockIncrement).toBeCalled();
});
it('Resetボタンが押されるとresetが呼ばれる', () => {
const mockReset = jest.fn();
jest.spyOn(CustomHookStoreModule, 'useCounter').mockReturnValueOnce({
count: 1,
increment: () => {},
reset: mockReset,
});
render(
<CustomHook />,
);
const button = screen.getByRole('button', { name: 'Reset' });
fireEvent.click(button);
expect(mockReset).toBeCalled();
});
});
/src/components/CustomHookStore.test.tsx的中文翻译如下:
自定义钩子存储测试.tsx
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './CustomHookStore';
describe('useCounter', () => {
describe('count', () => {
it('現在のcount値を返す', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
});
});
describe('increment', () => {
it('countを +1 する', () => {
const { result } = renderHook(() => useCounter(0));
act(() => { result.current.increment() });
expect(result.current.count).toBe(1);
});
});
describe('reset', () => {
it('countを 0 にする', () => {
const { result } = renderHook(() => useCounter(2));
act(() => { result.current.reset() });
expect(result.current.count).toBe(0);
});
});
});
-
- ロジックをコンポーネントから分離したので見通しが良く、それぞれの役割についてテストできるようになる
-
- コンポーネントはスナップショットテストによる見た目の変化と、イベントの動作(xxxを呼び出すなど)のみテスト。ロジックについてはテストしなくていい
- ロジック(カスタムフック)はその処理単体をテスト
There was a heated argument between two friends. One of them accused the other of betraying their trust, while the other defended their actions and claimed it was all a misunderstanding. The situation became tenser as both friends refused to back down and their voices grew louder. Their friendship seemed to be hanging by a thread, as neither side was willing to compromise or find a solution.
使用React的context API来定义全局状态
/src/Context.tsx的改写版
import React from 'react';
interface CountState {
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}
const CountContext = React.createContext<CountState>({
count: 0,
setCount: () => undefined,
});
interface CountProviderProps {
children: React.ReactNode;
}
export const CountProvider: React.FC<CountProviderProps> = ({ children }) => {
const [count, setCount] = React.useState<number>(0);
return (
<CountContext.Provider value={{ count, setCount }}>
{children}
</CountContext.Provider>
);
};
export const useCountValue = () => React.useContext(CountContext).count;
export const useCountSetValue = () => React.useContext(CountContext).setCount;
/src/components/Context.tsx可以用以下方式进行中文翻译:
/src/components/上下文.tsx
import React from 'react';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useCountValue, useCountSetValue } from '../Context';
const CountComponent: React.FC = () => {
const count = useCountValue();
console.log('CountComponent render');
return (
<Typography>Count: {count}</Typography>
);
};
const CountResetComponent: React.FC = () => {
const setCount = useCountSetValue();
console.log('ResetComponent render');
return (
<Button onClick={() => setCount(0)}>Reset</Button>
);
};
const CountUpComponent: React.FC = () => {
const setCount = useCountSetValue();
console.log('CountUpComponent render');
return (
<Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
);
};
const ContextComponent: React.FC = () => {
const navigate = useNavigate();
console.log('ContextComponent render');
return (
<Box>
<CountComponent />
<CountResetComponent />
<CountUpComponent />
<Button onClick={() => navigate('/use-state')}>go /use-state</Button>
</Box>
);
};
export default ContextComponent;
/src/App.tsx的中文本地化翻译
+ import { CountProvider } from './Context';
+ import Context from './components/Context';
+ interface ProvidersProps {
+ children: React.ReactNode;
+ }
+ export const Providers: React.FC<ProvidersProps> = ({ children }) => {
+ return (
+ <CountProvider>
+ {children}
+ </CountProvider>
+ );
+ };
const App: FC = () => (
- <BrowserRouter>
- <Routes>
- ...
- </Routes>
- </BrowserRouter>
+ <Providers>
+ <BrowserRouter>
+ <Routes>
+ ...
+ <Route path="/context" element={<Context />} />
+ ...
+ </Routes>
+ </BrowserRouter>
+ </Providers>
);
export default App;
-
- 全スコープで利用可能な状態を作ることができ、ログインユーザー情報など多数のコンポーネントで利用することができる
-
- local stateとgrobal stateについて
useStateで作っていた状態はlocal state
コンポーネントに閉じた状態で、unmountされると消える
contextで作る状態はgrobal state
全スコープで利用可能な状態で、リロードされるまで消えない
createContextで作ったコンテキスト(grobal state)を作り、それを利用するカスタムフックを作る
コンテキストは配下の子で利用可能。これにより構造的に離れたコンポーネントへのpropsバケツリレーがなくなる
コンテキストはRouterの親なので、全てのページ、全てのコンポーネントで利用可能なgrobal stateになる
どこからでもアクセスできるグローバルな状態はなるべく必要最低限に止める
优化上下文
使用React的context API来优化全局状态的渲染的例子。
/src/ContextOptimize.tsx 的原因
import React from 'react';
const CountContext = React.createContext<number>(0);
const SetCountContext = React.createContext<React.Dispatch<React.SetStateAction<number>>>(
() => undefined
);
interface CountProviderProps {
children: React.ReactNode;
}
export const CountOptimizeProvider: React.FC<CountProviderProps> = ({ children }) => {
const [count, setCount] = React.useState<number>(0);
return (
<CountContext.Provider value={count}>
<SetCountContext.Provider value={setCount}>
{children}
</SetCountContext.Provider>
</CountContext.Provider>
);
};
export const useCountValue = () => React.useContext(CountContext);
export const useCountSetValue = () => React.useContext(SetCountContext);
/src/components/ContextOptimize.tsx 的汉语翻译只需要一种处理方式。
import React from 'react';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useCountValue, useCountSetValue } from '../ContextOptimize';
const CountComponent: React.FC = () => {
const count = useCountValue();
console.log('CountComponent render');
return (
<Typography>Count: {count}</Typography>
);
};
const CountResetComponent: React.FC = () => {
const setCount = useCountSetValue();
console.log('ResetComponent render');
return (
<Button onClick={() => setCount(0)}>Reset</Button>
);
};
const CountUpComponent: React.FC = () => {
const setCount = useCountSetValue();
console.log('CountUpComponent render');
return (
<Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
);
};
const ContextOptimizeComponent: React.FC = () => {
const navigate = useNavigate();
console.log('ContextOptimizeComponent render');
return (
<Box>
<CountComponent />
<CountResetComponent />
<CountUpComponent />
<Button onClick={() => navigate('/use-state')}>go /use-state</Button>
</Box>
);
};
export default ContextOptimizeComponent;
/src/App.tsx的中文本地化翻译:
+ import { CountOptimizeProvider } from './ContextOptimize';
+ import ContextOptimize from './components/ContextOptimize';
export const Providers: React.FC<ProvidersProps> = ({ children }) => {
return (
- <CountProvider>
- {children}
- </CountProvider>
+ <CountProvider>
+ <CountOptimizeProvider>
+ {children}
+ </CountOptimizeProvider>
+ </CountProvider>
);
};
const App: FC = () => (
<Providers>
<BrowserRouter>
<Routes>
...
+ <Route path="/context-optimize" element={<ContextOptimize />} />
...
</Routes>
</BrowserRouter>
</Providers>
);
export default App;
-
- contextの例にはレンダリング最適化の面で問題
+ボタンを押した際、値を利用しているCountComponent以外にもセッターを利用しているResetComponentやCountUpComponentも再レンダリングされる
値とセッター含め同一のcontextが作られているため、値だけが変化した場合もセッター利用側が無駄に再レンダリングされる
値とセッターを別々のcontextで定義する必要がある
+ボタンを押した際、値を利用しているCountComponentのみ再レンダリングされ、セッター利用してるResetComponentやCountUpComponentはレンダリングされない
反弹
使用状态管理库recoil的全局状态使用示例。
/src/components/Recoil.tsx 的中文翻译如下:
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { atom, useRecoilState } from 'recoil';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const state = atom<number>({
key: 'count',
default: 0,
});
const useCounter = () => {
const [count, setCount] = useRecoilState(state);
const increment = () => {
setCount((count) => count + 1);
};
const reset = () => {
setCount(0);
};
return { count, increment, reset };
};
const CountComponent: React.FC = () => {
const { count } = useCounter();
console.log('CountComponent render');
return (
<Typography>Count: {count}</Typography>
);
};
const CountResetComponent: React.FC = () => {
const { reset } = useCounter();
console.log('ResetComponent render');
return (
<Button onClick={() => reset()}>Reset</Button>
);
};
const CountUpComponent: React.FC = () => {
const { increment } = useCounter();
console.log('CountUpComponent render');
return (
<Button onClick={() => increment()}>+</Button>
);
};
const RecoilComponent: React.FC = () => {
const navigate = useNavigate();
console.log('RecoilComponent render');
return (
<Box>
<CountComponent />
<CountResetComponent />
<CountUpComponent />
<Button onClick={() => navigate('/use-state')}>go /use-state</Button>
</Box>
);
};
export default RecoilComponent;
/src/App.tsx 的内容
+ import { RecoilRoot } from 'recoil';
+ import Recoil from './components/Recoil';
export const Providers: React.FC<ProvidersProps> = ({ children }) => {
return (
- <CountProvider>
- <CountOptimizeProvider>
- {children}
- </CountOptimizeProvider>
- </CountProvider>
+ <CountProvider>
+ <CountOptimizeProvider>
+ <RecoilRoot>
+ {children}
+ </RecoilRoot>
+ </CountOptimizeProvider>
+ </CountProvider>
);
};
const App: FC = () => (
<Providers>
<BrowserRouter>
<Routes>
...
+ <Route path="/recoil" element={<Recoil />} />
...
</Routes>
</BrowserRouter>
</Providers>
);
export default App;
-
- recoilを使うとRecoilRootで囲むだけで、context apiのようにgrobal stateや値とセッター毎にProviderを用意する必要がなくなる
- atomの定義とuseStateライクなuseRecoilStateを使ってgrobal stateを定義可能
优化后座力
使用状态管理库recoil来优化全局状态的渲染的示例。
/src/components/RecoilOptimize.tsx的中文本地化翻译为以下选项中的一个:
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const state = atom<number>({
key: 'count-optimize',
default: 0,
});
const useCount = () => useRecoilValue(state);
const useSetCount = () => useSetRecoilState(state);
const CountComponent: React.FC = () => {
const count = useCount();
console.log('CountComponent render');
return (
<Typography>Count: {count}</Typography>
);
};
const CountResetComponent: React.FC = () => {
const setCount = useSetCount();
console.log('ResetComponent render');
return (
<Button onClick={() => setCount(0)}>Reset</Button>
);
};
const CountUpComponent: React.FC = () => {
const setCount = useSetCount();
console.log('CountUpComponent render');
return (
<Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
);
};
const RecoilOptimizeComponent: React.FC = () => {
const navigate = useNavigate();
console.log('RecoilOptimizeComponent render');
return (
<Box>
<CountComponent />
<CountResetComponent />
<CountUpComponent />
<Button onClick={() => navigate('/use-state')}>go /use-state</Button>
</Box>
);
};
export default RecoilOptimizeComponent;
/src/App.tsx的原文为英文,它的意思是”应用程序的主要文件是App.tsx”。在中文中可以这样表达:应用程序的主要文件是/App.tsx。
+ import RecoilOptimize from './components/RecoilOptimize';
const App: FC = () => (
<Providers>
<BrowserRouter>
<Routes>
...
+ <Route path="/recoil-optimize" element={<RecoilOptimize />} />
...
</Routes>
</BrowserRouter>
</Providers>
);
export default App;
-
- recoilの例ではcontextの例と同様レンダリング最適化の面で問題
+ボタンを押した際、値を利用しているCountComponent以外にもセッターを利用しているResetComponentやCountUpComponentも再レンダリングされる
値とセッター含め同一のcontextが作られているため、値だけが変化した場合もセッター利用側が無駄に再レンダリングされる
useRecoilStateの代わりに値を利用するuseRecoilValueやセッターを利用するuseSetRecoilStateを別々に定義する必要がある
+ボタンを押した際、値を利用しているCountComponentのみ再レンダリングされ、セッター利用してるResetComponentやCountUpComponentはレンダリングされない
反弹自定义钩子 zì zi)
将全局状态的逻辑与组件分离,使其具备可测试性的示例。
/src/components/RecoilCustomHookStore.tsx 的中文翻译选项:
import React from 'react';
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';
// カプセル化のために通常exportしない
// renderRecoilHookを利用した初期状態付きのテストをする場合はexport
export const state = atom<number>({
key: 'count-test',
default: 0,
});
export const useCount = () => useRecoilValue(state);
export const useIncrement = () => {
const setCount = useSetRecoilState(state);
return React.useCallback(
() => {
setCount((count) => count + 1);
},
[setCount],
);
};
export const useReset = () => {
const setCount = useSetRecoilState(state);
return React.useCallback(
() => {
setCount(0);
},
[setCount],
);
};
/src/components/RecoilCustomHook.tsx 的中文本地化翻译选项:
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useCount, useReset, useIncrement } from './RecoilCustomHookStore';
const CountComponent: React.FC = () => {
const count = useCount();
console.log('CountComponent render');
return (
<Typography>Count: {count}</Typography>
);
};
const CountResetComponent: React.FC = () => {
const reset = useReset();
console.log('ResetComponent render');
return (
<Button onClick={() => reset()}>Reset</Button>
);
};
const CountUpComponent: React.FC = () => {
const increment = useIncrement();
console.log('CountUpComponent render');
return (
<Button onClick={() => increment()}>+</Button>
);
};
const RecoilCustomHookComponent: React.FC = () => {
return (
<Box>
<CountComponent />
<CountResetComponent />
<CountUpComponent />
</Box>
);
};
export default RecoilCustomHookComponent;
/src/App.tsx 的中文本地化改写:
/src/App.tsx
+ import RecoilCustomHook from './components/RecoilCustomHook';
const App: FC = () => (
<Providers>
<BrowserRouter>
<Routes>
...
+ <Route path="/recoil-custom-hook" element={<RecoilCustomHook />} />
...
</Routes>
</BrowserRouter>
</Providers>
);
export default App;
- カスタムフックを定義することにより、ロジックをコンポーネントから分離できる
考试 shì)
/src/components/RecoilCustomHook.test.tsx的中文同义短语
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import RecoilCustomHook from './RecoilCustomHook';
import * as RecoilCustomHookStoreModule from './RecoilCustomHookStore';
afterEach(() => cleanup());
describe('RecoilCustomHook', () => {
it('RecoilCustomHookが表示される', () => {
const { asFragment } = render(
<RecoilRoot>
<RecoilCustomHook />
</RecoilRoot>,
);
expect(asFragment()).toMatchSnapshot();
});
it('+ボタンが押されるとincrementが呼ばれる', () => {
const mockIncrement = jest.fn();
jest.spyOn(RecoilCustomHookStoreModule, 'useIncrement').mockReturnValueOnce(mockIncrement);
render(
<RecoilRoot>
<RecoilCustomHook />
</RecoilRoot>,
);
const button = screen.getByRole('button', { name: '+' });
fireEvent.click(button);
expect(mockIncrement).toBeCalled();
});
it('Resetボタンが押されるとresetが呼ばれる', () => {
const mockReset = jest.fn();
jest.spyOn(RecoilCustomHookStoreModule, 'useReset').mockReturnValueOnce(mockReset);
render(
<RecoilRoot>
<RecoilCustomHook />
</RecoilRoot>,
);
const button = screen.getByRole('button', { name: 'Reset' });
fireEvent.click(button);
expect(mockReset).toBeCalled();
});
});
/src/components/RecoilCustomHookStore.test.tsx 的中文本地化翻译:/src/components/RecoilCustomHookStore测试.tsx
import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library';
import { state, useCount, useReset, useIncrement } from './RecoilCustomHookStore';
describe('useCount', () => {
it('現在のcount値を返す', () => {
const { result } = renderRecoilHook(useCount);
expect(result.current).toBe(0);
});
});
describe('useIncrement', () => {
it('countを +1 する', () => {
const { result } = renderRecoilHook(() => {
const count = useCount();
const increment = useIncrement();
return { count, increment };
});
act(() => { result.current.increment() });
expect(result.current.count).toBe(1);
});
});
describe('useReset', () => {
it('countを 0 にする', () => {
const { result } = renderRecoilHook(() => {
const count = useCount();
const reset = useReset();
return { count, reset };
}, {
states: [{ recoilState: state, initialValue: 1 }],
});
expect(result.current.count).toBe(1);
act(() => { result.current.reset() });
expect(result.current.count).toBe(0);
});
});
-
- ロジックをコンポーネントから分離したので見通しが良く、それぞれの役割についてテストできるようになる
-
- コンポーネントはスナップショットテストによる見た目の変化と、イベントの動作(xxxを呼び出すなど)のみテスト。ロジックについてはテストしなくていい
-
- ロジック(カスタムフック)はその処理単体をテスト
-
- 注意: カプセル化のため通常atomで作ったstateはexportしない
initialValueを設定可能なテストの利便性とstateを別で利用される危険性は表裏一体なので要検討
后坐力选择器
利用 recoil selector 获取计算值并抑制不必要的重新执行。
/src/components/RecoilSelector.tsx组件进行释义。
import React from 'react';
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const double = (value: number) => {
console.log('calculate double', value);
return value * 2;
};
const state1 = atom<number>({
key: 'recoil-selector-count1',
default: 0,
});
const doubleState1 = selector<number>({
key: 'recoil-selector-count-double1',
get: ({ get }) => {
const count = get(state1);
return double(count);
},
});
const useCounter1 = () => {
const [count, setCount] = useRecoilState(state1);
const doubleCount = useRecoilValue(doubleState1);
const increment = () => {
setCount((count) => count + 1);
};
return { count, doubleCount, increment };
};
const state2 = atom<number>({
key: 'recoil-selector-count2',
default: 0,
});
const useCounter2 = () => {
const [count, setCount] = useRecoilState(state2);
const increment = () => {
setCount((count) => count + 1);
};
const doubleCount = double(count);
return { count, doubleCount, increment };
};
const Count1Component: React.FC = () => {
const { count } = useCounter1();
return (
<Typography>Count1: {count}</Typography>
);
};
const DoubleCount1Component: React.FC = () => {
const { doubleCount } = useCounter1();
return (
<Typography>DoubleCount1: {doubleCount}</Typography>
);
};
const CountUp1Component: React.FC = () => {
const { increment } = useCounter1();
return (
<Button onClick={() => increment()}>+</Button>
);
};
const Count2Component: React.FC = () => {
const { count } = useCounter2();
return (
<Typography>Count2: {count}</Typography>
);
};
const DoubleCount2Component: React.FC = () => {
const { doubleCount } = useCounter2();
return (
<Typography>DoubleCount2: {doubleCount}</Typography>
);
};
const CountUp2Component: React.FC = () => {
const { increment } = useCounter2();
return (
<Button onClick={() => increment()}>+</Button>
);
};
const RecoilSelectorComponent: React.FC = () => {
return (
<Box>
<Count1Component />
<DoubleCount1Component />
<CountUp1Component />
<Count2Component />
<DoubleCount2Component />
<CountUp2Component />
</Box>
);
};
export default RecoilSelectorComponent;
/src/App.tsx的内容。
+ import RecoilSelector from './components/RecoilSelector';
const App: FC = () => (
<Providers>
<BrowserRouter>
<Routes>
...
+ <Route path="/recoil-selector" element={<RecoilSelector />} />
...
</Routes>
</BrowserRouter>
</Providers>
);
export default App;
-
- recoil selectorを利用した計算値の取得と不要な再実行の抑制
-
- selectorを利用していない場合、フックを利用しているコンポーネント分再計算される
- selectorを利用している場合、useRecoilValueに渡して取得できる計算値の定義ができ、そのselector内部で利用しているatomが変更されない限り再実行は抑制される