はじめに
こんにちは、@Sicut_studyです。
Reactを勉強するとまず最初に勉強するのがuseStateなどのHooksだったと思います。
useState
やuseEffect
などは利用する場面が多く慣れている方も多いと思いますが、その他のHooksはどうでしょうか?そもそも名前すら知らないというHooksがたくさんあるかと思います。
その中には利用することでパフォーマンスを向上させたり、ステートを簡単に扱えるようになるものなど便利なものがたくさん用意されています。
React19の登場でuseActionState
やuseOptimistic
など絶対に覚えて活用していきたい重要なHooksも登場しております。
この記事ではそんなReactで用意されている全てのHooksを12分で読める内容にして紹介していきます。
最後まで読めばどのタイミングでどのHooksを選択すればよいかわかるようになるので、よりReactを使いこなせるようになり差別化できるかと思います。
動画教材も用意しています
こちらの教材にはより詳しく解説した動画もセットでご用意しています。
テキスト教材でわからない細かい箇所があれば動画も活用ください
対象者
- Reactを勉強し始めた方
- もっとReactを深く理解したい方
- 新しいHooksを理解したい方
- 短時間で学びたい方
React Hooksの全体像
まずはReact Hooksの全体像について理解していきましょう。
現在Reactでは19個のHooksと1個のAPIが用意されています。
React19になってuse
というAPI(関数)も追加されました。
この記事ではこれまでのHooksの使用頻度が高いものから順番に紹介していき、最後にReact19で追加されてものを紹介していきます。
01. useState
useStateを使って状態管理をすると、ステートが変更される度に再レンダリングが行われて画面が更新します。
const UseState = () => {
const [text, setText] = useState('');
return (
<div className="p-4">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
className="border border-gray-300 rounded px-2 py-1 mb-2"
/>
<div>入力されている値: {text}</div>
</div>
);
}
たとえばこのコンポーネントの場合、インプットフォームの値が更新されるとステートの更新関数に入力された内容が渡り、ステートが更新されます。
<input
type="text"
value={text}
// e.target.valueは入力された値
onChange={(e) => setText(e.target.value)}
useStateを使わない場合は画面が更新されません。
この例は変数の値を直接変えていますが、画面には反映されていないです。
ショッピングカートの合計金額の計算などにも利用できます。
02. useEffect
useEffectのよくある勘違いがあります。
useEffectでデータ取得はしないほうがいいです
useEffectでデータを取得するような以下のコードはパフォーマンスの観点からよくないです。
useEffect(() => {
// ユーザーデータの取得
const fetchUserData = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUserData(data);
};
理由としては大きく2つあります。
- ウォーターフォール問題
親コンポーネントのデータフェッチが完了するまで、子コンポーネントはデータフェッチが行われないのでパフォーマンスが落ちてしまう
- レースコンディションの問題
複数のリクエストが同時に発生した場合、古いリクエスト(遅い)の
結果が新しいリクエスト(速い)の結果を上書きしてしまう可能性がある。
function SearchComponent() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
// 検索を実行する
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => setResults(data));
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
このような商品を検索するシステムがあるとします。
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => setResults(data));
このAPIの返答速度が
検索語:りんご
応答時間:3秒(重い検索)
検索語:りんごジュース
応答時間:1秒(軽い検索)
としたときに「りんご」と検索したあとすぐに「りんごジュース」と検索すると画面には「りんご」の検索が反映されてしまいま
そこでデータ取得をするならReact Query
やSWR
などを利用しましょう。
これらのライブラリを使うことで「キャッシュ」「自動再取得」「エラーハンドリング」などを簡単に実現できます。
03. useReducer
アクションという概念を理解するためにカウンターの例を載せます
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
増やす
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
減らす
</button>
</div>
);
}
ボタンにタイプを指定することでreducerの中でどのボタンが押されたかを判定してステートを更新しています。
それぞれのボタンにonClickPlus1
やonClickminus1
のような関数を用意して指定する必要がなく、1つの関数を使いまわすことができています。
useReducer
を使うと良いタイミングは大きく2つあります。
- 複数の状態が関連している場合
// useStateだと別々に管理
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [isValid, setIsValid] = useState(false)
// useReducerなら一つのオブジェクトで管理できる
const [formState, dispatch] = useReducer(reducer, {
name: '',
email: '',
isValid: false
})
このような場合に利用することでそれぞれの項目ごとにuseStateを用意しなくて良くなります。
ただこの場合はReact Hook Formなどを利用するほうがよいでしょう。
2.状態の更新ロジックが複雑な場合
- 条件分岐が多い
- 複数の状態を同時に更新する必要がある
- バリデーションなど、追加の処理が必要
このようなときにuseReducerを使うことでuseStateより扱いやすくなります。
04. useContext
useContextを利用することでグローバルに値を持つことができるので好きなコンポーネントから値を直接使うことが可能です。
useContextを利用しない場合「Propsのバケツリレー」を行わないといけないです。
例えば赤色の親コンポーネントから末端のコンポーネントに値を受け渡す場合、関係ないコンポーネントに渡していかないといけないので大変です。
05. useRef
DOM参照や再レンダリングせずに値を保持できるフックです。
利用シーンには「フォームの入力要素への参照」「スクロール位置の制御」「タイマーIDの保持」などがあります。
1. DOMの直接アクセス
const MyComponent = () => {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>フォーカスを当てる</button>
</div>
);
};
useRefで目印を用意してインプットフォームに紐付けをします。
const inputRef = useRef(null);
<input ref={inputRef} type="text" />
ボタンをクリックしたら目印(inputRef)を使ってinput要素を取得しフォーカス直接実行します。
const focusInput = () => {
inputRef.current.focus();
};
2. 再レンダリングせずに保持
refを使うことで再レンダリングせずに値を保持することも可能です。
const MyComponent = () => {
const countRef = useRef(0);
const handleClick = () => {
countRef.current = countRef.current + 1;
console.log('クリック回数:', countRef.current);
};
return <button onClick={handleClick}>クリック</button>;
};
useStateを使うと値が更新する度に再レンダリングが発生します。
しかし、この例のように画面に関係ない値を保持しておくケースではuseRefを使うことで再レンダリングなしにカウントの値を保持しておくことができます。
06. useMemo
React19以降を利用している場合は不要
useMemoを使用しないと以下のケースでパフォーマンスが悪くなってしまいます。
// useMemoを使わない場合
function MovieList({ movies, searchTerm }) {
const filteredMovies = movies.filter(movie =>
movie.title.includes(searchTerm)
);
return (
<ul>
{filteredMovies.map(movie => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
);
}
このコンポーネントを使っている親コンポーネントがあるとします。
その親コンポーネントの何かしらのステートが更新されるとmoviesが変更されていなくても再レンダリングが行われてしまいます。
07. useCallback
React19以降を利用している場合は不要
useMemoが値のメモ化をするのに対して、関数のメモ化をするのがuseCallbackです。
使うシーンは子コンポーネントがメモ化されているときです。
// 親コンポーネント
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("クリック");
}, []);
return (
<>
<ChildComponent onClick={handleClick} />
<div>カウント: {count}</div>
</>
);
}
const ChildComponent = React.memo(({ onClick }) => {
console.log("子コンポーネントが再レンダリング");
return <button onClick={onClick}>クリック</button>;
});
この場合、親コンポーネントのcountが更新されても子コンポーネントの再レンダリングは行われません。ただしメモ化していないコンポーネントに渡した場合は意味がないです。
08. useLayoutEffect
DOMの変更を同期的に処理するためのフックです。
イメージしやすいようにuseEffectではうまくいかないコードをみてみましょう。
function FlickerComponent() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
setPosition({ x: window.innerWidth - 100, y: window.innerHeight - 100 });
}, []);
return (
<div
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`
}}
>
Hello World
</div>
);
}
この例ではHello WorldのDivを初期表示で画面の外(みえないように)しようとしています。
しかしuseEffectを利用すると一瞬Hello Worldが画面に表示されてしまいます。
そこでuseLayoutEffect
を利用することができます。
function NoFlickerComponent() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useLayoutEffect(() => {
setPosition({ x: window.innerWidth - 100, y: window.innerHeight - 100 });
}, []);
return (
<div
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`
}}
>
Hello World
</div>
);
}
それぞれの画面の更新タイミングがことなっているため、useLayoutEffectのほうが適しています。
09. useTransition
優先度の低い状態更新を後回しにできるフックです。
「ページネーションの切り替え」や「検索機能」「タブの切り替え」に利用できます。
function Search() {
const [searchTerm, setSearchTerm] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
// 入力値の更新は即時に
setSearchTerm(e.target.value);
// 検索結果の更新は後回し
startTransition(() => {
// 大量のデータを検索する処理(時間がかかる)
});
};
return (
<>
<input value={searchTerm} onChange={handleSearch} />
{isPending && <div>更新中...</div>}
</>
);
}
処理に時間がかかる検索などは、検索している途中ユーザーが操作しても画面が反応しなくなってしまいユーザー体験が落ちてしまいます。そこで検索処理の優先度をuseTransition
で落とすことでUXを向上できます。
トランジション関数と処理中かを表すisPending(true/false)を用意して
const [isPending, startTransition] = useTransition();
時間のかかる処理をstartTransition
の中で呼び出すようにします。
startTransition(() => {
// 大量のデータを検索する処理(時間がかかる)
});
処理が終わるまでisPendingはtrue
となるので利用してローディングをだすことも可能です。
{isPending && <div>更新中...</div>}
10. useDeferredValue
値の更新を遅延させることができるフックです。
useTransitionは関数自体を遅延していますが、useDeferredValueは値を遅延できます。
このHooksの利用シーンは「利用シーン : サードパーティのコンポーネントを使うなど、コードを直接制御できない場合」にあります。
import React, { useState, useDeferredValue } from 'react';
import ExpensiveThirdPartyList from 'some-third-party-library';
function SearchableList() {
const [searchQuery, setSearchQuery] = useState('');
const deferredQuery = useDeferredValue(searchQuery);
const handleSearch = (e) => {
setSearchQuery(e.target.value);
};
return (
<div>
<input
type="text"
value={searchQuery}
onChange={handleSearch}
placeholder="検索..."
/>
{/* サードパーティのコンポーネントに遅延された値を渡す */}
<ExpensiveThirdPartyList
searchText={deferredQuery}
// その他のprops...
/>
</div>
);
}
export default SearchableList;
このように処理を直接制御できないサードパーティのコンポーネントでも使用できます。
ExpensiveThirdPartyListの再レンダリングに時間がかかったとしても、遅延しているためUXがよくなります。
11. useId
一意のIDを生成するためのフックです。
主にアクセシビリティやSSRの安全性のために利用します。
1. ラベルとフォームの紐付け (アクセシビリティ)
function NameInput() {
const id = useId();
return (
<div>
<label htmlFor={id}>名前:</label>
<input id={id} type="text" />
</div>
);
}
使い方はuseId
を呼び出してidを取得するだけです。
インプットフォームとラベルを紐付けるのに利用しています。
2. 複数IDが必要な場合 (SSRの安全性)
function Form() {
const id = useId();
return (
<form>
<div>
<label htmlFor={`${id}-name`}>名前:</label>
<input id={`${id}-name`} type="text" />
</div>
<div>
<label htmlFor={`${id}-email`}>メール:</label>
<input id={`${id}-email`} type="email" />
</div>
</form>
);
}
このようなときにランダムなIDを使うと、サーバーサイドレンダリング(SSR)で不整合が起きる可能性があります。(同じIDがつく可能性)
12. useSyncExternalStore
外部ストア(Redux等)からデータを安全に読み取るためのフックです。
利用することが滅多にないフックで、独自の状態管理ライブラリなどを作る際に利用します。
// カスタムストアを作成する例
const createStore = (initialState) => {
let state = initialState;
const listeners = new Set();
return {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
getState() {
return state;
},
setState(newState) {
state = newState;
listeners.forEach(listener => listener());
}
};
};
Reduxなどを利用しないとなかなか使う機会はすくないです。
ただ以下のようにブラウザAPIを使って画面サイズを取得することなどには利用できるかもしれません。
// ブラウザAPIと連携する例
function WindowSize() {
const size = useSyncExternalStore(
// ウィンドウサイズの変更を監視
(onStoreChange) => {
window.addEventListener('resize', onStoreChange);
return () => window.removeEventListener('resize', onStoreChange);
},
// 現在のサイズを取得
() => ({
width: window.innerWidth,
height: window.innerHeight
})
);
return <div>Window size: {size.width} x {size.height}</div>;
}
13. useDebugValue
React DevToolsでデバックをしやすくするフックです。
React DevToolsを使っていない場合は使用しません。
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// フェッチの処理が入る
}, [url]);
useDebugValue({
url,
state: loading ? 'loading' : error ? 'error' : 'success'
});
return { data, loading, error };
}
useDebugValueに値をいれることでReact DevToolsでコンソール表示が可能です。
14. useImperativeHandle
子コンポーネントのrefで公開する値をカスタマイズするためのフックです。
// 親コンポーネント
function Parent() {
const inputRef = useRef();
const handleClick = () => {
// 子コンポーネントのメソッドを呼び出し
inputRef.current.focus();
inputRef.current.clear();
};
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>
フォーカス&クリア
</button>
</div>
);
}
import { forwardRef, useImperativeHandle } from 'react';
// forwardRefと組み合わせて使う
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 親コンポーネントに公開するメソッドを定義
useImperativeHandle(ref, () => ({
// カスタムメソッド
focus: () => {
inputRef.current.focus();
},
// 入力値をクリア
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
親のボタンをクリックすることで子コンポーネントのインプットフォームを操作することができています。
ちなみにforwardRef
というのを子コンポーネントで利用しているのは、React19以前はref
をPropsで渡すことができなかったからです。
しかし、React19になってrefを渡せるようになったので19以降を利用している場合は通常のPropsと同じように渡すだけで問題ありません。
15. useInsertionEffectReact
CSS-in-JSライブラリの実装者向けの特殊なフックです。
DOMの変更前に実行され、レイアウトの計算前にスタイルを挿入できます。
useLayoutEffectやuseEffectより先に実行されます。
import { useInsertionEffect } from 'react';
function useCssInJs(rule) {
useInsertionEffect(() => {
// DOMへのスタイル挿入
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => {
// クリーンアップ
document.head.removeChild(style);
};
}, [rule]);
}
16. useActionState
非同期処理をしたあとの結果でステート更新ができるフックです。
useTransition + useReducerのような機能で、非同期処理を待っている間はisPendingを利用してローディング画面などをだすことも可能です。そのあと処理が終わったらステートが更新されるので、画面の更新(再レンダリング)が行われます。
const initialPosts = [];
const [posts, getPosts, isPending] = useActionState(
async (currentPosts, payload) => {
const response = await fetch('https://api.example.com/posts');
const newPosts = await response.json();
return newPosts;
},
initialPosts
);
const handleClick = () => {
getPosts();
};
return (
<div>
<button onClick={handleClick} disabled={isPending}>
{isPending ? '読み込み中...' : '投稿を取得'}
</button>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
まずはuseActionsStateを呼び出します。
const [posts, getPosts, isPending] = useActionState(
async (currentPosts, payload) => {
const response = await fetch('https://api.example.com/posts');
const newPosts = await response.json();
return newPosts;
},
initialPosts
);
ここでpostsがステートの値、getPostsが引数の関数、isPendingがgetPostsが実行しているかを表すフラグ(true/false)になっています。第二引数のは初期値です。
<button onClick={handleClick} disabled={isPending}>
{isPending ? '読み込み中...' : '投稿を取得'}
</button>
ボタンがクリックされるとgetPostsが実行されてローディングが表示されます。
getPoostの中では新しいpostsを取得してリターンすることで、postsのステートを変更できます。
17. useOptimistic
楽観的更新を行うためのフックです。
楽観的更新とはユーザーのアクション結果を即座に画面に反映して、あとから実際の処理を行うことでUXを向上させるアプローチです。
function LikeButton() {
const [likes, setLikes] = useState(0);
// optimisticLikesは楽観的な値、addOptimisticLikeは更新関数
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state) => state + 1
);
const handleClick = async () => {
// 楽観的に即座にカウントアップ
addOptimisticLike();
// 重い処理(3秒かかると仮定)
await fetch('/api/like', { method: 'POST' });
// 実際のデータで更新
setLikes(likes + 1);
};
return (
<button onClick={handleClick}>
いいね!({optimisticLikes})
</button>
);
}
useOptimisticの楽観的なステートoptimisticLikes
と即座に更新するときに利用する関数をaddOptimisticLike
を用意します。
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state) => state + 1
);
第一引数には実際のステート、第二引数には楽観的更新をするときに適応する関数をいれています。
ボタンがクリックしたらaddOptimistic
を実行してすぐに+1したものを画面に表示します。
addOptimisticLike();
そして重い処理が終わってから実際のステートを更新します。
// 重い処理(3秒かかると仮定)
await fetch('/api/like', { method: 'POST' });
// 実際のデータで更新
setLikes(likes + 1);
もし途中で何かしらの例外が発生した場合楽観的なステートもすぐにもとのステートに戻り再レンダリングされます。
18. useFormState
フォームのステート管理とサーバーアクションの結果を扱うためのフックです。
async function submitForm(prevState, formData) {
// 入力内容の取得
const name = formData.get('name');
// バリデーションなど
if (!name) {
return { error: '名前を入力してください' };
}
// APIリクエストなど
await saveToDatabase(name);
return { success: true, message: '保存しました' };
}
function Form() {
const [state, formAction] = useFormState(submitForm, {
error: null,
success: false,
message: null
});
return (
<form action={formAction}>
<input type="text" name="name" />
{/* エラー表示 */}
{state.error && <p style={{color: 'red'}}>{state.error}</p>}
{/* 成功メッセージ */}
{state.success && <p>{state.message}</p>}
<button type="submit">送信</button>
</form>
);
}
useFormStateを使うことでuseActionStateをフォームに特化して行うことが可能です。
流れはほとんど変わりません。
async function submitForm(prevState, formData) {
// 入力内容の取得
const name = formData.get('name');
// バリデーションなど
if (!name) {
return { error: '名前を入力してください' };
}
// APIリクエストなど
await saveToDatabase(name);
return { success: true, message: '保存しました' };
}
function Form() {
const [state, formAction] = useFormState(submitForm, {
error: null,
success: false,
message: null
});
ステートを更新する関数を第一引数に渡して、初期値を第二引数に渡しています。
19. useFormStatus
フォームの送信状態を管理するためのフックです。
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '送信中...' : '送信'}
</button>
);
}
function Form() {
return (
<form action={async () => { /* ... */ }}>
<input type="text" name="name" />
<SubmitButton />
</form>
);
}
useFormStatusは子コンポーネントで行う必要があり、親コンポーネントのform属性のサブミットの状況をisPendingのフラグ(true/false)で取得することができます。
20. use
非同期データを扱うためのAPIです。
useはReact Hooksではなく、APIと呼ばれています。
たとえばuseEffect
でデータを取得するとこのようにかけます。
function Example1() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(json => setData(json));
}, []);
return <div>{data?.message}</div>;
}
useを利用するとPromiseを直接利用することができます。
function Example2() {
const data = use(fetch('https://api.example.com/data').then(res => res.json()));
return <div>{data.message}</div>;
}
useStateやuseEffectなどを利用せずにデータ取得ができます。
ただしローディング状態を把握することができないのでSuspenseと利用するの良いでしょう。
おわりに
いかがでしたでしょうか?
Hooksの理解自体はそこまで難しくなかったかと思います。
React19のHooksはどれも実用的で今後利用していくことが多くなるかと思います。詳しく解説した動画を投稿しているのでよかったらみてみてください!
次回のハンズオンのレビュアーはXにて募集します。
図解ハンズオンたくさん投稿しています!