環境
- windows10
- DockerDesktop for Win 3.5.x
- node 16.13.1
- npm 8.1.2
- TypeScript 4.5
- React 17.0.1
- Next.js 12.0.7
- Recoil 0.5.2
- VsCode
- gitbash 2.32.0.1
State
- コンポーネントの状態管理のための機能
- 基本的には、親コンポーネントでstateを管理する。(子コンポーネントで親コンポーネントのstateを変更したい場合は、stateのsetter(setState)を子コンポーネントに渡し、子から親のstateを変更する
import React, { useState } from 'react'
const Parent = () => {
const [count, setCount] = useState(0) // stateとstateを更新するためのsetterを宣言する
..割愛
return <Child> // 子コンポーネントからstateを更新したい場合はsetterをpropsでわたす
}
Hooks
概要
- Hooksは、React 16.8で追加された新機能で、関数コンポーネントでstateを扱えるようになる機能
- Hooksは独自に作成することも可能。(カスタムフックと呼ばれる。ほかのフックを呼び出せるJavaScriptの関数)。コンポーネントからロジック部分だけを抽出して再利用可能なHooksを作ることが可能
- Hooksの追加により、クラスコンポーネントでしか使用できなかったライフサイクルとstateが関数コンポーネントでも扱えるようになった
- Hooksにはルールがあり、順守していない場合は以下のようなエラーが発生する
Unhandled Runtime Error Error: Invalid hook call.
Hooks can only be called inside of the body of a function component.
This could happen for one of the following reasons:
Hooksのルール
エラーの原因はいくつか考えられるが、おおよそ以下の内容に抵触しているケースが多い
- Hooksは関数コンポーネントもしくはカスタムフックのトップレベルでのみ呼び出すことが可能(関数コンポーネントで定義されている関数の中や、useEffectなどの標準のHooksから呼び出すことも不可)
- React hooksは、すべてuse〇〇から始まるというルールがあるため、
カスタムフックも必ずuse〇〇という命名ルールに従う必要がある
frontend\next-web\src\components\provider\AuthProvider.tsx
import { useRouter } from 'next/router'e
const AuthProvider = ({ children }) => {
// トップレベルなのでOK
const router = useRouter()
const { listenAuthState } = useListenAuthState()
const somethingFunction = () => {
// トップレベルじゃないのでNG
// const router = useRouter()
// const { func } = useListenAuthState()
}
useEffect(() => {
listenAuthState()
}, [])
return children
}
export default AuthProvider
frontend\next-web\src\components\shared\function\Auth\AuthLisnter.tsx
..割愛
// カスタムフックの命名規則(use〇〇)
export const useListenAuthState = () => {
// カスタムフックのトップレベルなので、hooks呼び出し可能
const [user, setUser] = useRecoilState(userState)
const router = useRouter()
const listenAuthState = () => {
axios
.get('/api/user')
...割愛
return {
listenAuthState
}
}
HooksのsetStateは非同期処理
- setStateでstateを更新直後にlog出力しても、非同期処理なのでこの段階ではstateはまだ更新されていない
- 更新する値を別途用意して、それをsetStateで更新し、後続の処理で使用する
- もしくは、stateの変更によってComponentが再描画されてから処理したい場合は、setStateの第二引数にコールバック関数を渡すことで、stateの更新を待ってから後続の関数の処理を実行することが可能になる(prevStateは更新前のstate)
const [count, setCount] = useState(0)
...
// NGパターン
const countUp = () => {
setCount(count + 1)
console.log(count) // 1が取得されることを期待しているが、実際は0
}
// OKパターン1 (更新用の変数を先に定義してそれをsetterに渡す)
const countUp = () => {
const newCount = count + 1
setCount(newCount)
console.log(newCount)
}
// OKパターン2(コールバック関数をsetterに渡す)
const countUp = () => {
setCount((prevState) => {
const newState = prevState + 1
console.log(newState)
return newState
})
}
無限レンダリング
下記のようなエラー
Too many re-renders. React limits the number of renders to prevent an infinite loop.
React学習し始めのころに出くわしがちなエラー。以下のようなケースが多い
- setStateでstateを直接更新すると無限ループする
・render => setState => state更新 => render => setState=> state更新 => render ....
・useEffectなどで回避 - render内(jsx内)で関数を実行する形式で記述すると無限ループする(execute())
あらかじめ変数に代入して関数を渡すか、コールバック関数の形(() => execute())で指定する
NGな書き方
const EditStock = () => {
const [productName, setProductName] = useState('')
setProductName(st.name)// 割愛
OKな書き方
- 後述のuseEffectなどで一度だけ実行されることを保証する
- useEffectは第二引数の依存関係に空配列を指定すると、内部の処理が一度だけ実行される
const EditStock = () => {
const element = document.getElementById('stock-edit')
const [stock, setStock] = useState([])
const [productName, setProductName] = useState('')
useEffect(() => {
const st = JSON.parse(element.dataset.stock_edit)
setProductName(st.name)
}, [])
// 割愛
ライフサイクル
基本
- useEffectを使用することで、関数コンポーネントでも実現可能になる
- useEffectは関数コンポーネント内で副作用(関数コンポーネントの出力(レンダリング)に関係ない処理のこと)を実行するためのHooks(副作用とレンダリングを分離させることが可能になる)
- useEffectの基本的な書き方
・第一引数には副作用の処理を記述するためのコールバック関数を指定します。レンダリング後に実行されます
・useEffectの戻り値はundifined(返り値がない)かクリーンアップ関数(後述の、useEffectの第二引数で返す関数)である必要がある(= Promise)
・第二引数には依存先の変数が格納される配列が渡されます。これによって副作用の処理をどのタイミングで実行するか指定することができます
useEffect(callback[, dependencies]);
- useEffectの戻り値はundifinedかクリーンアップ関数である必要がある
・よくあるのが、以下のように、useEffectの中で非同期処理を行おうとする場合
// NG(第一引数の関数の戻り値がPromise型として設定されるのでエラー)
useEffect(async() => {
// 何かしらの非同期処理
}, [])
// Good
useEffect(() => {
// 何かしらの非同期処理
(async () => {
// await
})()
}, [])
- Reactのライフサイクルは大きく以下の流れ
- マウント(初回レンダリング=ブラウザリロード)
- アップデート
- アンマウント
- useEffectの第二引数の値に応じて、処理のタイミングが変動するので、実装したい処理に応じて使い分ける
// 毎回実行
useEffect(() => {
listenAuthState()
})
// 初回レンダリング後のみ実行(stateが更新されても、実行されない)
useEffect(() => {
listenAuthState()
}, [])
// 初回レンダリング後と、someThingDependenciesが変更された時のみ実行(通常はstateを指定する)
useEffect(() => {
listenAuthState()
}, [someThingDependencies])
クリーンアップ
- コンポーネントがアンマウントされる直前に必ず実行される関数
- 主に非同期処理などの時間を要する処理をuseEffect内に記述する際に必要となる
・非同期処理実行中に、ページ遷移等によりコンポーネントがアンマウントした場合、メモリリークのエラーが生じるため、アンマウント時に処理をクリーンアップする関数を実行する必要がある
useEffect(() => {
let isSubscribed = true
if (isSubscribed) {
axios.get('/api/posts').then((res) => {
const posts = res.data.posts
setPosts(posts)
})
}
return () => (isSubscribed = false) // アンマウント時に必ず購読フラグをfalseに
}, [])
以上でPart2は終了です。Part3はこちら
https://maasaablog.com/?p=3481
まとめ
いかがでしたでしょうか。本記事では、全5部構成でReactの基礎知識や知っておくと便利なTIPSについて紹介しています。対象者としてはReactの初学者~中級者を対象としており、Reactの基本的な書き方は知っているが、細かい処理や仕組みについては理解できていないという方には役に立つのではないかと思います