title |
---|
useState |
useState
は、コンポーネントに state 変数 を追加するための React フックです。
const [state, setState] = useState(initialState)
コンポーネントのトップレベルで useState
を呼び出して、state 変数を宣言します。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
state 変数は慣習として、分割代入を利用して [something, setSomething]
のように命名します。
initialState
: state の初期値です。どんな型の値でも渡すことができますが、関数を渡した場合は特別な振る舞いをします。この引数は初回レンダー後は無視されます。initialState
に関数を渡した場合、その関数は初期化関数 (initializer function) として扱われます。初期化関数は、純粋で、引数を取らず、何らかの型の値を返す必要があります。React はコンポーネントを初期化するときに初期化関数を呼び出し、その返り値を初期 state として保存します。例を見る
useState
は以下の 2 つの値を持つ配列を返します。
- 現在の state。初回レンダー中では、
initialState
と同じ値になります。 - state を別の値に更新し、再レンダーをトリガする
set
関数。
useState
はフックであるため、コンポーネントのトップレベルまたはカスタムフック内でのみ呼び出すことができます。ループや条件文の中で呼び出すことはできません。これが必要な場合は、新しいコンポーネントを抽出し、state を移動させてください。- Strict Mode では、純粋でない関数を見つけやすくするために、初期化関数が 2 回呼び出されます。これは開発時のみの振る舞いであり、本番には影響しません。初期化関数が純粋であれば(そうであるべきです)、2 回呼び出されてもコードに影響はありません。2 回の呼び出しのうち 1 回の呼び出し結果は無視されます。
useState
が返す set
関数を利用して、state を別の値に更新し、再レンダーをトリガすることができます。直接次の state を渡すか、前の state から次の state を導出する関数を渡します。
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
nextState
: 次に state にセットしたい値です。どんな型の値でも渡すことができますが、関数を渡した場合は特別な振る舞いをします。nextState
に関数を渡した場合、その関数は更新用関数 (updater function) として扱われます。更新用関数は、純粋で、処理中の state の値を唯一の引数として受け取り、次の state を返す必要があります。React は更新用関数をキューに入れ、コンポーネントを再レンダーします。次のレンダーで、React はキューに入れられたすべての更新用関数を前の state に対して適用し、次の state を導出します。例を見る
set
関数は返り値を持ちません。
-
set
関数は次のレンダーのための state 変数のみを更新します。set
関数を呼び出した後に state 変数を読み取っても、呼び出し前の画面に表示されていた古い値が返されます。 -
新しい値が現在の
state
と同一の場合、React は最適化のために、コンポーネントとその子コンポーネントの再レンダーをスキップします。state の同一性の比較は、Object.is
によって行われます。一部のケースでは、React は子コンポーネントをスキップする前にコンポーネントを呼び出す必要がありますが、あなたのコードに影響を与えることはないはずです。 -
React は state の更新をまとめて行います(バッチ処理)。すべてのイベントハンドラを実行し終え、
set
関数が呼び出された後に、画面を更新します。これにより、1 つのイベント中に複数回の再レンダーが発生することはありません。まれに、早期に画面を更新する必要がある場合(例えば DOM にアクセスする場合など)がありますが、その場合はflushSync
を利用できます。 -
set
関数は常に同一のものとなるため、多くの場合エフェクトの依存配列では省略されますが、依存配列に含めてもエフェクトの再実行は起こりません。依存値を削除してもリンタがエラーを出さない場合、削除しても安全です。エフェクトから依存値を取り除く方法を参照してください。 -
レンダー中に
set
関数を呼び出すことは、現在レンダー中のコンポーネント内からのみ許されています。その場合、React はその出力を破棄し、新しい state で再レンダーを試みます。このパターンが必要になることはほとんどありませんが、前回のレンダーからの情報を保存するために使用できます。例を見る -
Strict Mode では、純粋でない関数を見つけやすくするために更新用関数が 2 回呼び出されます。これは開発時のみの振る舞いであり、本番には影響しません。更新用関数が純粋であれば(そうであるべきです)、2 回呼び出されてもコードに影響はありません。2 回の呼び出しのうち 1 回の呼び出し結果は無視されます。
コンポーネントのトップレベルで useState
を呼び出し、1 つ以上の state 変数を宣言します。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
state 変数は慣習として、分割代入を利用して [something, setSomething]
のように命名します。
useState
は、以下の 2 つの値を持つ配列を返します。
- この state 変数の現在の値。最初は、初期 state に設定されます。
- インタラクションに応じて、state を他の値に変更するための
set
関数。
スクリーン上の表示を更新するには、次の state を引数として set
関数を呼び出します。
function handleClick() {
setName('Robin');
}
React は次の state を保存したあと、新しい値でコンポーネントを再レンダーし、UI を更新します。
set
関数の呼び出しは、既に実行中のコードの現在の state を変更するわけではありません。
function handleClick() {
setName('Robin');
console.log(name); // Still "Taylor"!
}
この呼び出しは、次のレンダー以降に useState
が返す値にのみ影響を与えます。
この例では、count
state 変数が数値 (number) を保持しています。ボタンをクリックすることで、数値が増加します。
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
この例では、text
state 変数が文字列 (string) を保持しています。ブラウザの DOM の input 要素に文字を入力すると、handleChange
は input 要素から最新の値を読み出し、setText
を呼び出して state を更新します。これにより、下部に、現在の text
を表示できます。
import { useState } from 'react';
export default function MyInput() {
const [text, setText] = useState('hello');
function handleChange(e) {
setText(e.target.value);
}
return (
<>
<input value={text} onChange={handleChange} />
<p>You typed: {text}</p>
<button onClick={() => setText('hello')}>
Reset
</button>
</>
);
}
この例では、liked
state 変数が真偽値 (boolean) を保持しています。input をクリックすると、setLiked
はブラウザのチェックボックスの input がチェックされているかどうかで、liked
state 変数を更新します。liked
変数は、チェックボックスの下のテキストをレンダーするために使用されます。
import { useState } from 'react';
export default function MyCheckbox() {
const [liked, setLiked] = useState(true);
function handleChange(e) {
setLiked(e.target.checked);
}
return (
<>
<label>
<input
type="checkbox"
checked={liked}
onChange={handleChange}
/>
I liked this
</label>
<p>You {liked ? 'liked' : 'did not like'} this.</p>
</>
);
}
同じコンポーネントで、複数の state 変数を宣言することができます。それぞれの state 変数は、完全に独立しています。
import { useState } from 'react';
export default function Form() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => setAge(age + 1)}>
Increment age
</button>
<p>Hello, {name}. You are {age}.</p>
</>
);
}
button { display: block; margin-top: 10px; }
age
が 42
である場合を考えましょう。このハンドラは、setAge(age + 1)
を 3 回呼び出します。
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
しかし、1 回クリックしたあと、age
は 45
ではなく 43
になります! これは、set
関数を呼び出しても、既に実行されているコードの age
state 変数を更新するわけではないためです。そのため、setAge(age + 1)
の呼び出しはすべて setAge(43)
になります。
この問題を解消するため、次の state の代わりに、更新用関数を setAge
に渡すことができます。
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
ここで、a => a + 1
は更新用関数です。更新用関数は、処理中の state の値を受け取り、そこから次の state を導出します。
React は更新用関数をキューに入れます。そして、次のレンダー中に、同じ順番で更新用関数を呼び出します。
a => a + 1
は処理中の state の値として42
を受け取り、次の state として43
を返します。a => a + 1
は処理中の state の値として43
を受け取り、次の state として44
を返します。a => a + 1
は処理中の state の値として44
を受け取り、次の state として45
を返します。
キューにはこれ以上の更新用関数はないので、React は最終的に 45
を現在の state として保存します。
慣習として、処理中の state の引数名には、state 変数名の頭文字 1 文字を利用することが一般的です(例えば、age
という state 変数に対して、a
という引数名)。しかし、prevAge
など、他の分かりやすい名前を使うこともできます。
開発時に更新用関数が 2 回呼び出されることがあります。これは、更新用関数が純粋であることを確認するためです。
新しくセットする値が直前の state から導出される場合、常に setAge(a => a + 1)
という書き方をすべきだという意見があります。悪いことではありませんが、必ずしも必要なわけではありません。
ほとんどのケースでは、どちらのアプローチでも違いはありません。React は、クリックなどのユーザの意図的なアクションに対して、age
state 変数の更新が次のクリックの前に発生することを保証しています。すなわち、イベントハンドラの開始時に、クリックハンドラが「古い」age
を参照してしまうことはありません。
一方で、同じイベント内で複数回の更新を行う場合、更新用関数が役に立ちます。また、state 変数自身を参照することが難しいケースにも有用です(再レンダーの発生を最適化する際に、このケースに遭遇することがあります)。
わずかな文法の冗長性よりも一貫性を優先するのであれば、state が直前の state から導出される場合には、常に更新用関数を書くようにすることは合理的です。もし、state が、他の state 変数の直前の値から導出される場合は、それらを 1 つのオブジェクトにまとめてリデューサ (reducer) を利用することを検討してください。
この例では更新用関数を渡しているため、"+3" ボタンは想定通りに動きます。
import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(a => a + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<button onClick={() => {
increment();
}}>+1</button>
</>
);
}
button { display: block; margin: 10px; font-size: 20px; }
h1 { display: block; margin: 10px; }
この例では更新用関数を渡していません。そのため "+3" ボタンは意図した通りには動きません。
import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(age + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<button onClick={() => {
increment();
}}>+1</button>
</>
);
}
button { display: block; margin: 10px; font-size: 20px; }
h1 { display: block; margin: 10px; }
state にオブジェクトや配列をセットすることができます。ただし React では、state は読み取り専用として扱う必要があります。そのため、state を更新する場合は、既存のオブジェクトを直接書き換える (mutate) のではなく、置き換える (replace) 必要があります。例えば、state として form
オブジェクトを保持している場合、以下のように書き換えを行ってはいけません。
// 🚩 Don't mutate an object in state like this:
form.firstName = 'Taylor';
代わりに、新しいオブジェクトを作成してオブジェクト全体を置き換えてください。
// ✅ Replace state with a new object
setForm({
...form,
firstName: 'Taylor'
});
詳しくは、state 内のオブジェクトの更新や、state 内の配列の更新を参照してください。
この例では、form
state 変数はオブジェクトを保持しています。それぞれの input 要素は change ハンドラを持っており、新しい form
オブジェクトを引数として setForm
を呼び出します。{ ...form }
のようにスプレッド構文を用いることで、state オブジェクトを(書き換えではなく)確実に置き換えることができます。
import { useState } from 'react';
export default function Form() {
const [form, setForm] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]',
});
return (
<>
<label>
First name:
<input
value={form.firstName}
onChange={e => {
setForm({
...form,
firstName: e.target.value
});
}}
/>
</label>
<label>
Last name:
<input
value={form.lastName}
onChange={e => {
setForm({
...form,
lastName: e.target.value
});
}}
/>
</label>
<label>
Email:
<input
value={form.email}
onChange={e => {
setForm({
...form,
email: e.target.value
});
}}
/>
</label>
<p>
{form.firstName}{' '}
{form.lastName}{' '}
({form.email})
</p>
</>
);
}
label { display: block; }
input { margin-left: 5px; }
この例では、state がネストされたオブジェクトになっています。ネストされたオブジェクトの state を更新する場合、更新するオブジェクトのコピーを作成する必要があります。さらに、そのオブジェクトを内包する上位のオブジェクトも同様に、コピーを作成する必要があります。詳しくは、ネストされたオブジェクトの更新を参照してください。
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
function handleCityChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
city: e.target.value
}
});
}
function handleImageChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
image: e.target.value
}
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
img { width: 200px; height: 200px; }
この例では、todos
state 変数が配列を保持しています。各ボタンのハンドラは、todos
配列の新しい値を引数として setTodos
を呼び出します。スプレッド構文 ([...todos]
) や、todos.map()
、todos.filter()
などを利用すると、state の配列を(書き換えではなく)確実に置き換えることができます。
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(initialTodos);
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
}
function handleChangeTodo(nextTodo) {
setTodos(todos.map(t => {
if (t.id === nextTodo.id) {
return nextTodo;
} else {
return t;
}
}));
}
function handleDeleteTodo(todoId) {
setTodos(
todos.filter(t => t.id !== todoId)
);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
配列やオブジェクトの書き換えを行わずに state を更新することが煩雑に感じる場合、Immer のようなライブラリを用いて繰り返しのコードを減らすことができます。Immer を利用することで、オブジェクトを書き換えているかのような簡潔なコードを書くことができます。しかし内部では、イミュータブル(不変, immutable)な更新が実行されます。
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [list, updateList] = useImmer(initialList);
function handleToggle(artworkId, nextSeen) {
updateList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={list}
onToggle={handleToggle} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
React は一度だけ初期 state を保存し、2 回目以降のレンダーではそれを無視します。
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
createInitialTodos()
は毎レンダーで呼び出されるものの、その結果は初回レンダーでのみ利用されます。これは、createInitialTodos()
が巨大な配列の生成やコストの高い計算を行っている場合に、無駄が多くなります。
これを解決するため、以下のように初期化関数を渡すことができます。
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
関数を呼び出した結果である createInitialTodos()
ではなく、createInitialTodos
という関数それ自体を渡していることに注意してください。useState
に関数が渡された場合、React は初期化時に、関数を一度だけ呼び出します。
初期化関数が純粋であることを確認するため、開発時に初期化関数が 2 回呼び出されることがあります。
この例では、初期化関数を利用しています。そのため、createInitialTodos
関数は初期化時のみ実行されます。入力フィールドに文字を入力した場合などの、コンポーネントの再レンダー時には実行されません。
import { useState } from 'react';
function createInitialTodos() {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: 'Item ' + (i + 1)
});
}
return initialTodos;
}
export default function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
const [text, setText] = useState('');
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
setTodos([{
id: todos.length,
text: text
}, ...todos]);
}}>Add</button>
<ul>
{todos.map(item => (
<li key={item.id}>
{item.text}
</li>
))}
</ul>
</>
);
}
この例では、初期化関数を利用していません。そのため、createInitialTodos
関数は、入力フィールドに文字を入力したときなどのすべてのレンダーで実行されます。挙動に目に見える違いはありませんが、少し効率が悪くなります。
import { useState } from 'react';
function createInitialTodos() {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: 'Item ' + (i + 1)
});
}
return initialTodos;
}
export default function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
const [text, setText] = useState('');
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
setTodos([{
id: todos.length,
text: text
}, ...todos]);
}}>Add</button>
<ul>
{todos.map(item => (
<li key={item.id}>
{item.text}
</li>
))}
</ul>
</>
);
}
key
属性は、リストをレンダーする場合によく利用します。しかし、もう 1 つの使い道があります。
コンポーネントに異なる key
を渡すことで、コンポーネントの state をリセットすることができます。この例では、version
state 変数を Form
に key
として渡しています。"Reset" ボタンをクリックすると、version
state 変数が変化します。key
が変化したとき、React は Form
コンポーネント(と、そのすべての子コンポーネント)を一から再生成するため、Form
の state がリセットされます。
詳しくは、state の保持とリセットを参照してください。
import { useState } from 'react';
export default function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
function Form() {
const [name, setName] = useState('Taylor');
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Hello, {name}.</p>
</>
);
}
button { display: block; margin-bottom: 20px; }
通常、state の更新はイベントハンドラの中で行われます。しかし、レンダーに応じて state を設定したい場合があります。例えば、prop が変化したときに state 変数を変化させたい場合です。
以下に示すように、ほとんどのケースでは不要です。
- もし必要な値が現在の props と他の state のみから導出される場合、冗長な state を削除してください。もし何度も再計算されることが気になる場合は、
useMemo
フックが役に立ちます。 - もしコンポーネントツリーの state 全体をリセットしたい場合、コンポーネントに異なる
key
を渡してください。 - 可能であれば、関連するすべての state をイベントハンドラの中で更新してください。
これらがどれも適用できない稀なケースでは、コンポーネントのレンダー中に set
関数を呼び出し、それまでにレンダーされた値に基づいて state を更新するパターンが利用できます。
以下の例では、CountLabel
コンポーネントは、渡された count
プロパティを表示しています。
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
直近の変更で、counter の値が増えたのか減ったのかを表示したいとします。count
プロパティだけでは知ることができないため、前回の値を保持し続ける必要があります。前回の値を保持するために、prevCount
state 変数を追加します。さらに、trend
state 変数を追加し、count が増えたのか減ったのかを保持します。prevCount
と count
を比較し、もしこれらが一致しない場合に、prevCount
と trend
を更新します。これで、現在の count プロパティと、前回のレンダーからどのように変化したのかの両方を表示することができます。
import { useState } from 'react';
import CountLabel from './CountLabel.js';
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
<CountLabel count={count} />
</>
);
}
import { useState } from 'react';
export default function CountLabel({ count }) {
const [prevCount, setPrevCount] = useState(count);
const [trend, setTrend] = useState(null);
if (prevCount !== count) {
setPrevCount(count);
setTrend(count > prevCount ? 'increasing' : 'decreasing');
}
return (
<>
<h1>{count}</h1>
{trend && <p>The count is {trend}</p>}
</>
);
}
button { margin-bottom: 10px; }
レンダー中に set
関数を呼び出す場合は、prevCount !== count
のような条件文の中でなければならず、その中には setPrevCount(count)
のような呼び出しが書かれなければならないことに注意してください。さもないと、再レンダーのループに陥り、コンポーネントがクラッシュします。また、例のように、現在レンダーしているコンポーネントの state のみ更新することができます。レンダー中に別のコンポーネントの set
関数を呼び出すとエラーになります。最後に、set
関数の呼び出しは、書き換えなしで state を更新する必要があります。これは、純関数の他のルールを破ることができないことを意味します。
このパターンは理解するのが難しいため、通常は避けるべきです。しかし、エフェクト内で state を更新するよりは良い方法です。レンダー中に set
関数を呼び出すと、コンポーネントが return
文で終了した直後、子コンポーネントをレンダーする前に再レンダーが行われます。このため、子コンポーネントが 2 回レンダーされずに済みます。コンポーネント関数の残りの部分は引き続き実行されます(結果は破棄されますが)。もし、set
関数の呼び出しを含む条件分岐が、すべてのフックの呼び出しより下にある場合、早期 return;
を追加して、再レンダーを早めることができます。
set
関数の呼び出しは、実行中のコードの state を変化させません。
function handleClick() {
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
setTimeout(() => {
console.log(count); // Also 0!
}, 5000);
}
これは、state がスナップショットのように振る舞うためです。state の更新は、新しい state の値での再レンダーをリクエストします。すでに実行中のイベントハンドラ内の count
という JavaScript 変数には影響を与えません。
次の state が必要な場合は、set
関数に渡す前に一度変数に保存することができます。
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
React では、更新の前後で state の値が変化しない場合、その変更は無視されます。state の値の変化は、Object.is
によって判断されます。この現象は、state のオブジェクトや配列を直接書き換えた場合によく起こります。
obj.x = 10; // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything
既存の obj
オブジェクトを書き換えて、setObj
に戻したため、この更新は無視されます。修正するには、state のオブジェクトや配列を書き換えるのではなく、置き換える必要があります。
// ✅ Correct: creating a new object
setObj({
...obj,
x: 10
});
Too many re-renders. React limits the number of renders to prevent an infinite loop.
というエラーが出ることがあります。これは通常、レンダー中に無条件に set
関数を呼び出しているため、コンポーネントがループに入っていることを意味します。レンダー、set
関数の呼び出し(レンダーを引き起こす)、レンダー、set
関数の呼び出し(レンダーを引き起こす)、というように続きます。大抵の場合、これはイベントハンドラの指定を間違ったことによるものです。
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
このエラーの原因がわからない場合は、コンソールのエラーの横にある矢印をクリックして、JavaScript スタックを調べ、エラーの原因となる set
関数の呼び出しを特定してください。
Strict Mode では、いくつかの関数が、本来 1 回のところを 2 回呼び出されることがあります。
function TodoList() {
// This component function will run twice for every render.
const [todos, setTodos] = useState(() => {
// This initializer function will run twice during initialization.
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// This updater function will run twice for every click.
return [...prevTodos, createTodo()];
});
}
// ...
これは予想される動作であり、あなたのコードを壊すものではありません。
これは開発時のみの挙動で、コンポーネントを純粋に保つために役立ちます。React は、呼び出し結果の 1 つを利用し、もう 1 つを無視します。コンポーネント、初期化関数、更新用関数が純粋であれば、この挙動があなたのロジックに影響を与えることはありません。ただし、誤って純粋でない関数を指定した場合は、これにより間違いに気付くことができるでしょう。
例えば以下の更新用関数は、state の配列を書き換えるため純粋ではありません。
setTodos(prevTodos => {
// 🚩 Mistake: mutating state
prevTodos.push(createTodo());
});
React は更新用関数を 2 回呼び出すため、todo が 2 つ追加されてしまい、間違いに気付くことができます。この例では、配列を書き換えるのではなく、置き換えることで間違いを修正できます。
setTodos(prevTodos => {
// ✅ Correct: replacing with new state
return [...prevTodos, createTodo()];
});
更新用関数が純粋になったため、複数回呼び出されても動作に影響しません。これが、2 回呼び出されることで間違いに気付くことができる理由です。コンポーネント、初期化関数、更新用関数のみが純粋である必要があります。イベントハンドラは、純粋である必要がないため、2 回呼び出されることはありません。
詳しくは、コンポーネントを純粋に保つを参照してください。
関数を state にセットしようとすると、その関数が呼び出されてしまう {/im-trying-to-set-state-to-a-function-but-it-gets-called-instead/}
このような形で関数を state に設定することはできません。
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
関数を渡すと、React は someFunction
を初期化関数、someOtherFunction
を更新用関数として扱います。そのため、それらを呼び出し、その結果を保存しようとします。関数を実行するのではなく保存するには、どちらの場合も () =>
を前に付ける必要があります。こうすると、React は関数自体を保存します。
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}