この記事は https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e の日本語訳です。途中で力尽きて雑な翻訳になってる箇所がいくつかあります。
私は、新しい Hooks の API の "magic" な挙動に悩んでいる人が結構いると聞いています。なのでこの構文の提案が、少なくとも表面的なレベルではどう動いているか、解説してみようと思います。
Hooks のドキュメントには React コアチームの規定する、あなたが知るべき2つのルールがあります。
- Hooksをループの中、条件文の中、ネストした関数の中で呼んではいけない
- React Function からのみ Hooks を呼んでもいい
後者は明白でしょう。関数コンポーネントに振る舞いを付与するのに、あなたはその振る舞いとコンポーネントを紐付けることができる必要があります。
前者は、不自然に思えて、紛らわしいかもしれません。これについて説明しようと思います。
考え方を整理するために、簡単なHooksの実装を見てましょう。
これは、実装を考えるための、APIを実装する方法の一つでしかないことに注意してください。
どのようにAPIが内側で動いているか理解する必要はありません。これもまた一つの実装例です。将来的にこの動き方は変更される可能性があります。
hook が動くための実装の一つを実演するために、一つの例を解説していきます。
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi");
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
Hooks API の裏にあるアイデアは、あなたが hook 関数の返り値の、配列の二番目の要素として setter 関数を使うことを出来るようにして、その setter が hook によって state をコントロールすることをできるようにする、というものです。
Reactの中で何が起ころうとしているかを解説していきましょう。次の例は、あるコンポーネントのレンダリングで実行される、コンテキストの内側で起こることです。
これはここで保存されるデータが、レンダーされている間コンポーネントの外側で生存していることを意味します。
この状態は他のコンポーネントと共有されません。
2つの空の setters と state を生成します。
カーソルを 0 に設定します。
コンポーネント関数を初めて実行します。
どの useState() の呼び出しも、最初は、(カーソル位置に紐付けられた) setter 関数を setters の配列にプッシュし、state を state の配列にプッシュします。
どの二回目の render もカーソルはリセットされ、これらの値は単に配列から読まれます。
どの setter もそのカーソル位置への参照をもっていて、setterを実行することで、そのカーソル位置の state を書き換えます。
これがこの実装を実演するコードです。
let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;
function createSetter(cursor) {
return function setterWithCursor(newVal) {
state[cursor] = newVal;
};
}
// This is the pseudocode for the useState helper
export function useState(initVal) {
if (firstRun) {
state.push(initVal);
setters.push(createSetter(cursor));
firstRun = false;
}
const setter = setters[cursor];
const value = state[cursor];
cursor++;
return [value, setter];
}
// Our component code that uses hooks
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
const [lastName, setLastName] = useState("Yardley"); // cursor: 1
return (
<div>
<Button onClick={() => setFirstName("Richard")}>Richard</Button>
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
</div>
);
}
// This is sort of simulating Reacts rendering cycle
function MyComponent() {
cursor = 0; // resetting the cursor
return <RenderFunctionComponent />; // render
}
console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']
// click the 'Fred' button
console.log(state); // After-click: ['Fred', 'Yardley']
もし今、外部の要素の順番に基づいたこの hook の順番を変更すると、何が起こるでしょうか?
React team がやるべきではない、と言っていることをやってみましょう。
let firstRender = true;
function RenderFunctionComponent() {
let initName;
if(firstRender){
[initName] = useState("Rudi");
firstRender = false;
}
const [firstName, setFirstName] = useState(initName);
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
この時点では firstName と lastName には正しいデータが含まれていますが、二回目のレンダーを見てみましょう。
今、firstName と lastName の両方が Rudi
にセットされて一貫性がなくなっています。これはわかりやすくエラーで動いていないわけですが、これはなぜ hooks のルールがこのやり方でダメになるかの考え方を教えてくれます。
これらの規則を遵守しなければ、データの一貫性が失われます
これらのことから、条件文やループの中で use
hook を呼び出せないのはなぜか分かります。レンダー内の呼び出しの順序を変更すると、配列のカーソル位置を扱うのでカーソルが一致せず、正しいデータまたはハンドラーを指さなくなります。
このトリックは、一貫したカーソルが必要な hook の管理を考えるためのものです。これが意識できていれば、全てがうまくいくでしょう。
幸いなことに hook API の裏で何が起こっているかの考え方を整理することができたと思います。
ここで覚えておいてほしいことは、順序に気をつけることで、Hooks API は良い結果をもたらすということです。
Hooks は React Components への効果的なプラグインです。これは Hooks API に人々が熱狂している理由で、あなたはどこに配列のセットが存在しているか考えていれば、これらのルールを破らずに済むことが出来ます。
私は近い将来、useEffects メソッドを見てみて、React Component の Lifecycle と比較してみたいと思っています。
You can follow Rudi Yardley on Twitter as @rudiyardley or on Github as @ryardley