Dave Ceddia氏による全5回におよぶReact Hooks入門記事の第2回を本人の許可を得て意訳しました。
誤りやより良い表現などがあればご指摘頂けると助かります。
原文: https://daveceddia.com/usestate-hook-examples/
これまでは、ファンクションコンポーネントで書き始めて途中からstateが必要な局面になると、コンポーネントをクラスに書き換える必要がありました。
class Thing extends React.Component
を書き、関数の本体を render()
メソッド内に移植し、インデントを修正して、ようやくstateを追加することができます。
今日は、Hooksを使って同じ機能を手に入れることができます。
「フック」とは何でしょうか?良い質問です。Hooksについてはここで学んでください。
この記事では、特に useState
フックについて見ていきましょう。
メモ:Hooksは現在α版であり、プロダクション環境ではまだ使用できません。APIはさらに変更される可能性があるため、現時点ではプロダクションアプリの書き換えはオススメしません。Open RFCにコメントし、公式ドキュメントやFAQにも目を通してください。
useStateの仕事?
useState
フックはファンクションコンポーネントにstateを追加することができます。(「フック」と呼ばれるものは、実際には単なる関数であり、React 16.7 alphaにバンドルされています)。ファンクションコンポーネント内で useState
を呼ぶことで、stateの小片を作成することができます。
クラスでは、stateは常にオブジェクトであり、その内部にプロパティを保存できます。
Hooksでは、stateはオブジェクトである必要はありません。配列、数値、ブーリアン、文字列など、どんな型でも構いません。 useState
を呼ぶたびに、stateの小片を作成して単一の値を保存します。
実際の例を見た方が理解しやすいでしょう。
useStateを使ったコンポーネントの表示/非表示切り替えの例
この例は、「もっと見る」リンクが末尾についたテキストを表示するコンポーネントであり、リンクをクリックすると続きが表示されます。
ビデオチュートリアルの方がお好みであれば、似たコンポーネントの作成プロセスをこちらで鑑賞できます。
ここで起きていることを理解するためにコメントをよく読んでみてください。
// 最初に:reactからnamed exportされるuseStateをimportします
// このステップを省略して、React.useStateを使っても構いません
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
// このコンポーネントは2つのpropsを受け取ります:
// text - 表示するテキスト
// maxLength - 「もっと見る」の前に何文字表示するか
function LessText({ text, maxLength }) {
// stateの小片を作成して初期値をtrueにします
// hiddenはstateの現在の値で、
// setHiddenによって変更可能です
//
const [hidden, setHidden] = useState(true);
// テキストが十分に短ければ、ボタンの心配は無用です
if (text <= maxLength) {
return <span>{text}</span>;
}
// 開く/閉じるリンクによってテキストをレンダリング(短縮もしくはフル表示)します
// リンクがクリックされると、hiddenの値が更新されて
// 再レンダリングが行われます
return (
<span>
{hidden ? `${text.substr(0, maxLength).trim()} ...` : text}
{hidden ? (
<a onClick={() => setHidden(false)}> read more</a>
) : (
<a onClick={() => setHidden(true)}> read less</a>
)}
</span>
);
}
ReactDOM.render(
<LessText
text={`Focused, hard work is the real key
to success. Keep your eyes on the goal,
and just keep taking the next step
towards completing it.`}
maxLength={35}
/>,
document.querySelector('#root')
);
動作サンプルはこのCodeSandboxでお試しください!
たった一行のコードで、この関数をステートフルにしました。
const [hidden, setHidden] = useState(true);
一度実行されてしまえば、「もっと見る/閉じる」リンクはクリックされた時に setHidden
を呼び出すだけです。
useState
は2つの要素を配列で返し、ES6のデストラクチャリングで直接名前をつけています。最初の要素はstateの現在の値で、次の要素はstateの更新関数です。新しい値を渡して呼び出すだけで、stateは更新され、コンポーネントの再レンダリングが行われます。
const [hidden, setHidden] = useState(true);
しかしこの関数は一体何をしているんでしょうか?レンダリングごとに呼ばれるとするなら(実装そうなのですが)、どのようにstateを保持するのでしょうか?
トリッキーなHooks
ここで起きた「魔法」は、Reactがコンポーネントごとに隠しオブジェクトを保持していて、この永続的なオブジェクト内には「stateの部屋」で構成された配列があるということです。 useState
を呼ぶと、Reactは次の利用可能な部屋にstateを保存し、ポインター(配列のインデックス)をインクリメントします。
フックが常に同じ順番(Hooksのルールに従っている限り)で呼ばれると、Reactは特定の useState
呼び出しに対応する元の値を探すことができます。 useState
の最初の呼び出しは配列の最初に格納され、2番目の呼び出しは2番目に格納されるといった具合です。
これは魔法でも何でもなく、にわかには信じがたい1つの真実に基づいています。Reactはコンポーネントを呼び出すものなので、前もって準備しておくことができます。さらに言うと、コンポーネントのレンダリングは単なる関数呼び出しです。 <Thing />
のようなJSXは React.createElement(Thing)
にコンパイルされます。つまり、Reactはそれがいつどのように呼ばれるかを明確にコントロールすることができます。
この「呼び出し順」の魔法の仕組みが知りたければ、Hooks超入門の記事をご覧ください。
元のstateを踏まえたstate更新の例
もう1つ例を見てみましょう。元のstateを踏まえた更新です。
いわゆる「万歩計」を作ってみましょう。使うのはカンタンです。FitBitのようなものを想像してください。一歩歩くごとに、単純にボタンがクリックされます。1日の終わりに、何歩歩いたかを教えてくれます。これを読んでいる時、私は最初の資金調達に奔走しているでしょう。
function StepTracker() {
const [steps, setSteps] = useState(0);
function increment() {
setSteps(steps => steps + 1);
}
return (
<div>
Today you've taken {steps} steps!
<br />
<button onClick={increment}>I took another step</button>
</div>
);
}
この例は前のものに良く似ています。まず、 useState
で最初のstateの小片を作成し、初期値として0を設定します。 steps
の現在の値としての0と、更新用の関数を返します。万歩計をカウントアップするための increment
関数も持っています。
機能的な更新用関数としての setSteps
にお気づきでしょうか?単に setSteps(step + 1)
を呼んでも今回は同じ結果になるので、構わないのですが、更新用関数は、stateの古い値を持ったクロージャ内で更新が必要な時に役立ちます。更新用関数を使うことで、最新のstateを処理することが保証されます。
また、もう1つ重要なポイントとして、ボタンの onClick
propとしてインラインのアロー関数を書くのではなく、 increment
関数を抽出しています。このように書いても全く同じように動作します。
<button onClick={() => setSteps(steps => steps + 1)}>
I took another step
</button>
配列としてのstateの例
stateはどんな値でも保持できるんでしたよね!ランダムな数値のリストを例として作成しました。ボタンをクリックすると、新しいランダムな数値がリストに追加されます。
function RandomList() {
const [items, setItems] = useState([]);
const addItem = () => {
setItems([
...items,
{
id: items.length,
value: Math.random() * 100
}
]);
};
return (
<>
<button onClick={addItem}>Add a number</button>
<ul>
{items.map(item => (
<li key={item.id}>{item.value}</li>
))}
</ul>
</>
);
}
空配列 []
で初期化していることに注目してください。そして addItem
関数を確認してください。
state更新用の関数( 今回でいう setItems
)は古い値との「マージ」はせずに、新しい値で上書きします。そのため、配列に新しい項目を追加するには、ES6のスプレッド演算子 ...
を使って既存の項目を新しい配列にコピーしてから、新しい項目を末尾に挿入します。
複数のキーを持つstateの例
stateがオブジェクトの場合も見てみましょう。2つのフィールド(ユーザー名とパスワード)からなるログインフォームを作ります。
1つのstateオブジェクトで複数の値を保持して、個々の値を更新する方法について見ていきましょう。
function LoginForm() {
const [form, setValues] = useState({
username: '',
password: ''
});
const printValues = e => {
e.preventDefault();
console.log(form.username, form.password);
};
const updateField = e => {
setValues({
...form,
[e.target.name]: e.target.value
});
};
return (
<form onSubmit={printValues}>
<label>
Username:
<input
value={form.username}
name="username"
onChange={updateField}
/>
</label>
<br />
<label>
Password:
<input
value={form.password}
name="password"
type="password"
onChange={updateField}
/>
</label>
<br />
<button>Submit</button>
</form>
);
}
このCodeSandboxを試してみてください。
最初に、オブジェクトで初期化されたstateを作ります。
const [form, setValues] = useState({
username: '',
password: ''
});
クラスでstateを初期化するのと良く似ていますね。
送信処理用の関数では、 preventDefault
でページの更新を防ぎつつフォームの値を表示しています。
updateField
関数はもっと面白いです。 setValues
(state更新用関数と呼んでいる)を使って、オブジェクトを渡していますが、既存のstateが上書きされないように ...form
を使って既存の値が常に含まれるようにしています。 ...form
の行を取り出してどんな振る舞いになっているのか試してみてください。
最下部では、よく見慣れたJSXの塊があり、フォームと入力をレンダリングしています。 name
propを入力として渡したため、 updateField
関数は適切なstateを更新することができます。このようにすることで、フィールドごとのハンドラ関数を書かずに済みます。
useReducerを使ってもっとラクをする方法
useReducer
と呼ばれるフックもあります。これは複数のstateを管理するのにより適しているため、次回の投稿で見ていきましょう。
見逃しがないように登録してください!今週はHooksウィークなので、毎日新しい記事をお届けする予定です。