いまさら React に入門する
https://www.oreilly.co.jp/books/9784873119380/ これ買って一通り読んだから整理したい
これまでは Vue.js を中心に使ってきた。
プロジェクトのセットアップ
$ npx create-react-app test-app --template typescript --use-npm
$ code test-app
スクリプト
# 開発サーバ
$ npm start
# テスト
$ npm test # インタラクティブモード
# ビルド
$ npm run build
eject
は設定ファイルを出力するためのもの。
(これを npm script に入れておくのは微妙じゃないか?)
コンポーネント
ステートレス
import React from "react";
// 静的なコンポーネント
export const Test1 = () => <span>test1</span>
// props を使うコンポーネント
export const Test2: React.VFC<{msg: string, children: ReactNode}> = ({ msg, children }) => (
<div>
<p>{msg}</p>
{children}
</div>
)
children
は tsx でタグネストをした子要素のノードが入る。
型付けがちゃんと効くので警告が出る。
ステートフル
useState
// ローカルステート・イベントハンドラ
export const Test3 = () => {
const [count, setCount] = useState(0)
return <div>
<p>Count: {count}</p>
<button onClick={ ()=> setCount(count + 1) }>increment</button>
</div>
}
useReducer
export const Test4 = () => {
const [count, increment] = useReducer((a)=> a + 1,0)
return <div>
<p>Count: {count}</p>
<button onClick={ increment }>increment</button>
</div>
}
ステートから算出する値
Vue.js のcomputed 感がある。
count2
は依存している値を明示していないので再算出されない
基本的に再評価されるときに関数内は再評価されるらしいから、
明確に依存する値が変更したときだけしか計算してほしくないときに使う感じっぽい。
デメリットないなら、演算が必要なものは毎回こう書いてもいい気がするけど。
export const Test5 = () => {
const [count, increment] = useReducer((a) => a + 1, 0)
const count1 = useMemo(()=> count * 2, [count])
const count2 = useMemo(()=> count * 2, [])
return <div>
<p>Count: {count}</p>
<p>Count1: {count1}</p>
<p>Count2: {count2}</p>
<button onClick={ increment }>increment</button>
</div>
}
ライフサイクルフック
export const Test6: React.VFC<{ count: number }> = ({ count }) => {
useLayoutEffect(()=> console.log("Test6: before render"))
useEffect(()=> console.log("Test6: after render"))
return <p>Count: {count}</p>
}
export const Test7: React.VFC<{ onClick: ()=> void }> = (props) => {
useLayoutEffect(()=> console.log("Test7: before render"))
useEffect(()=> console.log("Test7: after render"))
return <button { ...props }>increment</button>
}
export const Test8 = () => {
const [count, increment] = useReducer((a: number) => a + 1, 0)
useLayoutEffect(()=> console.log("Test8: before render"))
useEffect(() => console.log("Test8: after render"))
return <>
<Test6 count={count} />
<Test7 onClick={ increment } />
</>
}
<></>
はフラグメント。ルートエレメントを複数にしたいときに使う。
useLayoutEffect
はコンポーネントレンダリング前に発火。
useEffect
はコンポーネントレンダリングあとに発火
メモ化
increment
がコールされると、count
カウントが更新される。
そうすると、 Test8
が再レンダリングされる。
親コンポーネントが再レンダリングされるときに、子コンポーネントの Test7
, Test6
も再レンダリングされる。
一方で Test7
と Test6
は純粋関数なので、パラメータが変化しない限りレンダリング結果が同じになる。
したがって、再レンダリングが行われる必要がない。
こういうときは memo
関数を使うとパラメータが変わらない限りコンポーネントの再レンダリング行われない。
Test7
をメモ化しておく。
export const Test7: React.VFC<{ onClick: ()=> void }> = memo((props) => {
useLayoutEffect(()=> console.log("Test7: before render"))
useEffect(()=> console.log("Test7: after render"))
return <button { ...props }>increment</button>
})
Test7
が再レンダリングされない。
Test8
が再レンダリングされるときに increment
が再生成されて、useCallBack
を使って... という感じになると思ったがそうではないらしい。それならよし。
Test8
にローカルステートを増やす
export const Test8 = () => {
const [count, increment] = useReducer((a: number) => a + 1, 0)
const [value, setValue] = useState("")
useLayoutEffect(()=> console.log("Test8: before render"))
useEffect(() => console.log("Test8: after render"))
return <>
<p>Value: {value}</p>
<input onInput={(e)=> setValue(e.currentTarget.value) } />
<Test6 count={count} />
<Test7 onClick={ increment } />
</>
}
Test6
が依存している count
が変化していないのに Test6
が再レンダリングされている。
Test6
を メモ化する。
export const Test6: React.VFC<{ count: number }> = memo(({ count }) => {
useLayoutEffect(()=> console.log("Test6: before render"))
useEffect(()=> console.log("Test6: after render"))
return <p>Count: {count}</p>
})
count
が変化したときだけ Test6
が再レンダリングされる。
Context
Vue.js の Provide / Inject と同じような使い方するやつ
import React, { createContext, useContext, useState } from "react";
const CounterContext = createContext<{
count: Readonly<number>;
increment: () => void;
decrement: () => void;
}
>({
count: 0,
increment: ()=> null,
decrement: ()=> null,
});
const increment = (count: number, updateCount: (v: number) => void) => () =>
updateCount(count + 1);
const decrement = (count: number, updateCount: (v: number) => void) => () =>
updateCount(count - 1);
export const CounterProvider: React.FC = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider
value={{
count: count,
increment: increment(count, setCount),
decrement: decrement(count, setCount),
}}
>
{children}
</CounterContext.Provider>
);
};
export const useCounter = () => useContext(CounterContext);
export const Test9 = () => {
const { count } = useCounter();
return <p> count * 2 = {count * 2} </p>;
};
export const Test10 = () => {
const { increment, decrement } = useCounter();
return (
<>
<p>
<button onClick={increment}>increment</button>{" "}
</p>
<p>
<button onClick={decrement}>decrement</button>{" "}
</p>
</>
);
};
export const App = ()=> <CounterProvider>
<Test9 />
<Test10 />
</CounterProvider>
ルーティング / SSR
どうせ Next.js 使うから別にいい。
エラーバウンダリ
サスペンズ
スタイル
これもNext.js 使うなら設定されてるっぽいからいいか。