更新:僕はこの話題についてReact Rally(Reactの開発者が集まる会合)でフォローアップトークをしました。この記事は「関数型setState」パターンに焦点を当てていますが、トークはsetStateを深く理解することに焦点を当てています。
React Rallyにおける僕のsetStateに関するトーク
ReactはJavaScriptで関数型プログラミングを普及させました。その結果、Reactが使用するコンポーネントベースのUIパターンを採用する巨大なフレームワークにつながりました。そして今、関数型に対する熱狂がウェブ開発エコシステム全体に広がっています。
The JavaScript ecosystem is moving from "new framework of the week" to "new (faster) React clone of the week" https://t.co/AIPhd4jhVe
— Sylvain Wallez (@bluxte) 2017年1月13日
JavaScriptのエコシステムは、「今週の新しいフレームワーク」から「今週の新しい(より速い)Reactクローン」へと移行しています。
Reactがどのように根本的変化をもたらしたか
しかし、Reactチームは弱まるどころではありません。彼らは、さらに深く掘り下げて、伝説の図書館に隠されたさらに多くの関数型の宝石を発見し続けています。
この記事では、Reactに埋もれた新しい関数型の金脈であり、最高の秘密を明かします。それは、関数型setStateです!
さて、僕は今その名前を付けました…正確には、それはまったく新しいものでも秘密のものでもありません。それはReactに組み込まれたパターンであり、深く掘り下げている少数の開発者だけが知っていることです。そしてそれには名前が付いていませんでした。でも今は名前がありますよ。それは、関数型setStateです!
このパターンを説明するDan Abramovの言葉によると、関数型setStateは以下のようなパターンです。
「コンポーネントクラスとは別にstate(状態)の変更を宣言します。」
えっ?
あなたが既に知っていること
ReactはコンポーネントベースのUIライブラリです。コンポーネントは、基本的にいくつかのプロパティを受け入れ、UI要素を返す関数です。
function User(props) {
return (
);
}
コンポーネントは、自身のstate(状態)を持ち、管理する必要があります。その場合、通常はコンポーネントをクラスとして記述します。次に、そのクラスのconstructor関数にstate(状態)を設定します。
class User {
constructor () {
this.state = {
score : 0
};
}
render () {
return (
);
}
}
state(状態)を管理するために、ReactはsetState()という特別なメソッドを提供しています。以下のように使います:
class User {
...
increaseScore () {
this.setState({score : this.state.score + 1});
}
...
}
setState()がどのように機能するかに注目してください。更新したいstate(状態)のオブジェクトを含む部分を渡すのです。つまり、渡すオブジェクトには、コンポーネントのstate(状態)のkeyに対応するkeyがあり、setState()はオブジェクトをstate(状態)にマージしてstate(状態)を更新または設定します。だから「set-State(stateの設定)」という名前です。
あなたが知らないであろうこと
setState()がどのように機能するかを覚えていますか?さて、ここで私が、オブジェクトを渡す代わりに、関数を渡すことができると言ったらどうなるでしょうか?
そうなんです。setState()は関数も受け入れます。関数は、コンポーネントの前のstate(状態)と現在のprops(親コンポーネントから渡されたプロパティ)を受け入れ、次のstate(状態)を計算して返します。以下を参照してください:
this.setState(function (state, props) {
return {
score: state.score - 1
}
});
setState()は関数であり、別の関数をそれに渡していることに注意してください(関数型プログラミング、関数型setState)。一見すると、これは醜いように見えるかもしれません。単にset-state(stateを設定)するにはあまりにも多くのステップがあるからです。こんなことをするメリットは何でしょうか?
なぜ関数をsetStateに渡すのか?
実は、state(状態)の更新は非同期である場合があります。
setState()が呼び出されたときに何が起こるかを考えてみてください。まずReactはsetState()に渡したオブジェクトを現在のstate(状態)にマージします。その後、reconciliation(調停)のようなものを開始します。それは新しいReact要素ツリー(UIのオブジェクト表現)を作成し、新しいツリーを古いツリーと比較し、setState()に渡したオブジェクトに基づいて何が変更されたのかを調べ、最終的にDOMを更新します。
ひゃー!とても大変です!実際、これでも概要を大幅に単純化したほどです。しかし、Reactを信頼してください!
Reactは単純に「set-state(stateを設定する)」のではありません。
関与している作業量によっては、setState()を呼び出しても、すぐにstate(状態)が更新されないことがあります。
Reactは、複数のsetState()呼び出しをパフォーマンス向上のために単一の更新にバッチすることができます。
このことに基づいて、Reactが持つ意味は何でしょうか?
まず、「複数のsetState()呼び出し」とは、以下のように1つの関数内でsetState()を複数回呼び出すことです:
...
state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
}
...
Reactが、「set-state(stateの設定)」を3回繰り返すのではなく、「複数のsetState()呼び出し」を検出したとき、Reactは上で説明した膨大な量の作業を避け、スマートに「いいえ!私はこの山を3回登るつもりではなく、1回の旅行ごとにstate(状態)の一部を運んで更新します。むしろコンテナを用意し、これらのstate(状態)の一部をまとめてパッキングして、この更新を一度だけ行います」と自分に言い聞かせます。読者の皆さん、そしてそれはバッチング(バッチ処理)です!
setState()に渡すものは単純なオブジェクトだということを覚えていますか。今度は、Reactが「複数のsetState()呼び出し」を検出したと仮定してみましょう。各setState()呼び出しに渡されたすべてのオブジェクトを抽出してバッチ処理を行い、それらをマージして単一のオブジェクトを形成し、その単一のオブジェクトを使用してsetState()を行います。
JavaScriptでは、マージするオブジェクトは以下のようになります:
const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3
);
このパターンはオブジェクトコンポジションと呼ばれます。
JavaScriptでは、オブジェクトをマージまたはコンポーズ(合成)する方法は、このように機能します。3つのオブジェクトが同じkeyを持つ場合、Object.assign()に渡された最後のオブジェクトのkeyの値が優先されるのです。例えば:
const me = {name : "Justice"},
you = {name : "Your name"},
we = Object.assign({}, me, you);
we.name === "Your name"; //true
console.log(we); // {name : "Your name"}
youがweにマージされた最後のオブジェクトであるため、youのオブジェクトのnameの値である「Your name」は、meのオブジェクトのnameの値よりも優先されます。だからweのオブジェクトにたどり着くのは「Your name」なのです。youの勝ち!
したがって、オブジェクトを複数回使用してsetState()を呼び出すと、つまり毎回オブジェクトを渡すと、Reactがマージします。言い換えると、Reactが渡された複数のオブジェクトの中から新しいオブジェクトをコンポーズ(合成)します。そして、いずれかのオブジェクトに同じkeyが含まれている場合、同じkeyを持つ最後のオブジェクトのkeyの値が格納されるんでしたよね?
つまり、上記のincreaseScoreBy3関数が与えられると、関数の最終結果は3ではなく1になります。なぜなら、ReactがsetState()という順序でstate(状態)を直ちに更新しなかったからです。しかし、最初にReactはすべてのオブジェクトをコンポーズ(合成)し、その結果は{score : this.state.score + 1}となります。そして、新しく作成されたオブジェクトで「set-state(stateの設定)」が一度だけ実行されます。こんな感じです:User.setState({score : this.state.score + 1}。
さらに明確にすると、オブジェクトをsetState()に渡すことが問題なのではありません。実際の問題は、前のstate(状態)から次のstate(状態)を計算するときにオブジェクトをsetState()に渡すことです。だから絶対にやめてください。危険です!
this.propsとthis.stateは非同期に更新される可能性があるため、次のstate(状態)を計算するために値を使用しないでください。
ここに、この問題をデモするSophia Shoemakerのcodepenがあります。少し遊んでみて、このcodepenの悪いソリューションと良いソリューションの両方に注意を払ってみましょう:
このcodepenの悪いソリューションと良いソリューションの両方に注意を払ってみましょう…
お助け関数型setState
上記のcodepenで遊ばなかった人へ:この記事の中心概念を理解するのに役立つので、ぜひ遊んでみてください。
あなたが上記のcodepenで遊んでいた間、関数型setStateが僕たちの問題を修正したことを間違いなく目撃しましたね。しかし、正確にはどのように機能したのでしょうか?
React界のOprah(アメリカのテレビ番組の司会者兼プロデューサー、オプラ・ウィンフリー)であるDanに相談してみましょう。
It is safe to call setState with a function multiple times. Updates will be queued and later executed in the order they were called. pic.twitter.com/xNr6EDVdJv
— Dan Abramov (@dan_abramov) 2017年1月25日
setStateを関数で複数回呼び出すことは安全です。更新はキューに入れられ、後で呼び出された順に実行されます。
彼の答えに注目してください。関数型setStateを実行すると…
更新はキューに入れられ、後で呼び出された順に実行されます。
したがって、Reactが、オブジェクトをまとめてマージする代わりに(もちろん、マージするオブジェクトはありません)「複数のsetState()呼び出し」を検出すると、Reactは関数を「呼び出された順に」キューに入れるのです。
その後、Reactは「キュー」内の各関数を呼び出すことによってstate(状態)を更新し、前のstate(状態)を渡します。前のstate(状態)とは、最初の関数型setState()呼び出しの前のstate(状態)(現在実行中の最初の関数型setState()の場合)、またはキュー内の前の関数型setState()呼び出しから最新の更新を伴うstate(状態)のことです。
繰り返しますが、僕はいくつかのコードを見るのが素晴らしいことだと思っています。しかし今回は、すべてを偽造するつもりです。これが本物ではないと知っていても、Reactが何をしているのか見当をつけることができます。
また、あまり冗長にしないために、ES6を使用します。必要に応じて、いつでもES5バージョンを書き込むことができます。
まず、コンポーネントクラスを作成しましょう。次に、その内部に、偽のsetState()メソッドを作成します。また、コンポーネントにはincreaseScoreBy3()メソッドがあり、これは複数の機能を持つsetStateを実行します。最後に、Reactと同じように、クラスをインスタンス化します。
class User{
state = {score : 0};
//let's fake setState
setState(state, callback) {
this.state = Object.assign({}, this.state, state);
if (callback) callback();
}
// multiple functional setState call
increaseScoreBy3 () {
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) )
}
}
const Justice = new User();
setStateはオプショナルな2番目のパラメータ、つまりコールバック関数も受け入れることに注目してください。コールバック関数が存在する場合、Reactはstate(状態)を更新した後にそれを呼び出します。
ユーザーがincreaseScoreBy3()をトリガーすると、Reactは複数の関数型setStateをキューに入れます。ここではまだロジックを偽造しません。僕たちの焦点は、実際に関数型setStateを安全に保つのは何かということだからです。しかし、以下のように、そのような「キューイング」プロセスの結果を一連の関数と考えることができます:
const updateQueue = [
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1})
];
最後に、更新プロセスを偽造しましょう:
// recursively update state in the order
function updateState(component, updateQueue) {
if (updateQueue.length === 1) {
return component.setState(updateQueue[0](component.state));
}
return component.setState(
updateQueue[0](component.state),
() =>
updateState( component, updateQueue.slice(1))
);
}
updateState(Justice, updateQueue);
確かに、これはセクシーなコードではありません。きっとあなたの方がきれいなコードを書けるでしょう。しかしここで重要なのは、Reactが関数型setStateから関数を実行するたびに、Reactは更新されたstate(状態)の新しいコピーを渡すことでstate(状態)を更新するということです。これにより、関数型setStateが前のstate(状態)に基づいてstate(状態)を設定することが可能になります。
これは、完全なコードのbinです。さらに感覚を養うために、(もっとセクシーに見えるように)いじくりまわしてください。
FunctionalSetStateInAction
このbinにあるコードをで遊ぶと楽しいですよ。忘れないでください!僕たちは考え方を学ぶためにReactを偽造しているだけです…
jsbin.com
完全に把握するためにそれで遊んでみてください。遊び終わったら、関数型setStateが本当に黄金の値打ちがあるものだとわかるでしょう。
Reactの最高の秘密
これまで、なぜReactで複数の関数型setStateを実行するのが安全なのかを深く探ってきました。しかし、実際には、関数型setStateの完全な定義をまだ実現していません。その完全な定義とは、「コンポーネントクラスとは別にstate(状態)の変更を宣言する」ということです。
長年に渡って、setting-state(つまり、setState()に渡す関数やオブジェクト)のロジックは、常にコンポーネントクラスの内部に存在していました。これは宣言的というよりも命令的です。
今日、僕は新しく発見した宝物を紹介します。Reactの最高の秘密は:
Best kept React secret: you can declare state changes separately from the component classes. pic.twitter.com/LczYP7yw2R
— Dan Abramov (@dan_abramov) 2017年1月25日
Reactの最高の秘密:コンポーネントクラスとは別にstate(状態)の変更を宣言できること。
ありがとう、Dan Abramov!
それが関数型setStateのパワーです。state(状態)更新ロジックをコンポーネントクラス外に宣言してください。次に、コンポーネントクラス内で呼び出してください。
// outside your component class
function increaseScore (state, props) {
return {score : state.score + 1}
}
class User{
...
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}
...
}
これは宣言的です!これであなたのコンポーネントクラスは、もはやstate(状態)がどのように更新されたかなんて気にすることはないでしょう。単に希望する更新のタイプを宣言するだけです。
さらに深く理解してみましょう。通常多くのstate(状態)スライスを持ち、それぞれのスライスを異なるアクションで更新する複雑なコンポーネントについて考えてみてください。そして時々、それぞれの更新関数は多くの行のコードを必要とします。このロジックはすべてコンポーネント内に存在していました。でももはやそうじゃありません!
また、僕はすべてのモジュールをできるだけ短く保つのが好きですが、今はモジュールが長くなりすぎているように感じます。あなたが僕と同じ考えならば、これで、すべてのstate(状態)変更ロジックを別のモジュールに抽出し、次にインポートしてコンポーネントで使用することができます。
import {increaseScore} from "../stateChanges";
class User{
...
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}
...
}
異なるコンポーネントでincreaseScore関数を再利用することもできます。単純にインポートしてみましょう。
他に関数型setStateでできることはありますか?
テストを簡単にすることです!
Declaring state updates as pure functions makes it a breeze to test complex state transitions. Even no need for shallow rendering! pic.twitter.com/VywwTjyCD2
— Dan Abramov (@dan_abramov) 2017年1月25日
state(状態)更新を純粋な関数として宣言することで、複雑なstate(状態)遷移をスムーズにテストすることができます。もうシャローレンダリングも必要ありません!
次のstate(状態)を計算するために余分な引数を渡すこともできます(このコードには非常に感動しました…#funfunFunction)。
What if you need to pass an extra argument to calculate the next state? Functions can return functions pic.twitter.com/eAtLHHeY6y
— Dan Abramov (@dan_abramov) 2017年1月25日
次のstate(状態)を計算するために余分な引数を渡す必要がある場合はどうなるのでしょう?関数は関数を返すことができますよ
さらに期待しましょう…
Reactの未来
数年前から、Reactチームはステートフルな関数を最もうまく実装する方法を実験してきました。
関数型setStateは、(おそらく)それにぴったりな答えのようです。
Danさん!何か最後に言いたいことありますか?
It’s likely that in future React will eventually support this pattern more directly so I recommend to try using it and see if you like it!
— Dan Abramov (@dan_abramov) 2017年1月25日
将来的にReactはこのパターンをより直接的にサポートする可能性が高いので、使用してみることをお勧めします!
ここまで全て理解していれば、僕の興奮もわかるでしょう。この関数型setStateを今日試してみてください!
僕がなかなかいい仕事をしたと思ってくださる方、または他の人が読むべき記事だと感じた方は、僕たちのコミュニティにおけるReactの理解を広げるために、下の緑のハートをクリックしてください。
未解決の疑問がある場合、またはこの記事の内容の一部に同意しない場合は、この記事にまたはTwitterを使用してコメントを投稿してください。
ハッピーコーディング!
Quincy Larsonに心からの感謝を送ります。
CREDIT:原著者の許諾のもと翻訳・掲載しています。
[原文]Functional setState is the future of React (Posted Mar 2, 2017) by Justice Mba