https://jp.quora.com/React-Hooks%E3%81%AFRedux%E3%82%88%E3%82%8A%E3%82%82%E4%BE%BF%E5%88%A9%E3%81%A7%E3%81%99%E3%81%8B
react-reduxは7.1以降Hooksとしても利用できるので、今やそれは排反の選択肢ではありません。
Hooks導入後のコンポーネントの状態管理の選択肢は、主なものとして
- useState単独
- useState+useContext
- useReducer単独
- useReducer+useContext
- action抽象を導入する
- action抽象をあえて導入しない
- React-Redux 7.1以降(useDispatch+useSelector)
- Redux Toolkit(旧名Redux Starter Kit)(5+Immer+ducks module+action creator自動生成)
があります(他にもあると思います)。
1はクラスコンポーネントでのstate相当ですが、それと全く同じバケツリレー問題を抱えていて、状態やその変更のためのコールバックを複数のコンポーネント間でとりまわすと早晩に限界が来ます。逆に、コンポーネント内に閉じた状態が主だったり、コールバックをとりまわさずに済むのであれば1でも問題ありません。
2は、個人的には中途半端に感じられます。集約の結果としてstateが大きくなれば、reducer的なものが必要になります。
3は意味ない気がします。
4aは実質5を自前でやることです。4に対する5の利点は、オレオレではないこと、reduxミドルウェアやredux devtoolが利用できることです。ミドルウェアの必要性は、各種hooksや導入予定のsuspenseによって低減されている・され得るとは思います。しかしredux-sagaの代替はできないでしょう。ただ確かに、4 vs. 5は考えどころです。
その上で6が登場してきたのですが、5の利点をすべて持ち、sagaなどももちろん使おうと思えば使え、さらに冗長性が低くなる仕掛けが効果的で、Reducerも破壊的操作のように簡潔にかけ(Immer効果)、4,5よりもメリットが大きいので、私としては6をおすすめします。
ちなみに4bはそういう記事を見たのですが、action抽象はビューからロジックの依存を逆転させ、ビューの再利用性や試験可能性を向上させます。ので私としては却下します。
まとめると、Hooks makes redux great again!です。
参考
そもそもReactは難しいものではないです。
仮想DOMとJSXについて少し調べれば、すぐに納得がいくはずなのです。
ましてやReduxなんてもっと単純です。
そこで、当回答ではいかにReduxの簡単かを説明することにします。
まず、Reduxのユースケースは複数コンポーネント間での状態共有です。
Reduxを知らない状況で、コンポーネント間で値を共有をしたいと思ったとき、みなさんだったらどうしますか?
一番簡単な方法を思い浮かべてください。
。
。
。
もちろん、ミュータブルなグローバル変数を使いますよね。
- export let state = {
- count: 0
- }
これだけです。
使う方は state.count++
とかすればいいのですが、この変更を他の利用モジュール(コンポーネント)にも通知したいです。
なので、状態を変更する関数を用意し、状態そのものは直接変更できないように隠ぺいし、なおかつ変更した際の通知を購読できるように利用側がフックの登録をできるようにします。
- // 初期状態
- let state = {
- count: 0
- }
- // 状態を変更する関数
- export function increment(num) {
- state.count += num
- // 状態が変われば、フックが呼び出される
- // フックには状態が引数として渡され、通知が行く
- hooks.forEach(hook => hook(state))
- }
- let hooks = []
- // フックを登録する関数
- export function subscribe(hook) {
- hooks.push(hook)
- // ついでに登録解除のための関数も返す
- return () => hooks.splice(hooks.findIndex(x => x === hook), 1)
- }
(ここの実装をEventEmitterでやろうがProxyでやろうがあなたの自由です。)
さて、ここまでの話はJavaScriptの基礎レベルの話です。
では早速これを簡単なカウンターコンポーネント内で使ってみましょう。
コンポーネント内に収まらない共有状態へのアクセスは副作用にあたるので、useEffect
を使うのが妥当でしょう。
- import React, { useState, useEffect } from "react"
- import { subscribe, increment } from "./共有状態"
- function Counter(props) {
- const [count, setCount] = useState(0)
- useEffect(() => {
- // フック
- const changeCount = (state) => setCount(state.count)
- // フックの登録
- const unsubscribe = subscribe(hook)
- // フックの解除
- return unsubscribe
- }, [count])
- return <span onClick="() => increment(1)">{count}</span>
- }
- export default Counter
これで望んでいた状態管理はできています。
あとは状態管理のモジュールを少しだけ改善するだけです。
まず、状態変更の為の関数をもう一度おさらいしてみましょう。
- export function increment(num) {
- state.count += num
- hooks.forEach(hook => hook(state))
- }
ここで、同じような関数がもう一つ加わった場合を考えてみます。
- export function increment(num) {
- state.count += num
- hooks.forEach(hook => hook(state))
- }
- export function decrement(num) {
- state.count -= num
- hooks.forEach(hook => hook(state))
- }
DRYじゃないですね。(怒り)
状態変更の部分と通知に分けると、通知部は共通化できそうです。
では状態変更部分はどうするかを考えましょう。
状態変更とは、現在状態A / 変更値から次の状態Bを計算することです。
(イメージを図にするとこんな感じです。)
これは、”2つの引数を取り1つの戻り値を返す関数”で表現できます。
そういう関数をReducer(リデューサー)と言います。(2→1に減らす(Reduce)からReducerです。)
先ほどの関数をReducerを使った形に変えると
- export function increment(change) {
- const reducer = (state, num) => {
- state.count += num
- return state
- }
- // reducerに現在の状態と変更値を渡すと、新しい状態をくれる
- state = reducer(state, change)
- // 新しい状態をフックに渡す
- hooks.forEach(hook => hook(state))
- }
ここまで変形すると、Reducerは一か所で管理して、どのReducerを利用するかを引数で指定できれば、increment
/ decrement
と分けずに一つの関数に処理を統一できそうです。
では、その関数を dispatch
関数としましょう。
- const reducers = {
- increment: (state, num) => {
- state.count += num
- return state
- },
- decrement: (state, num) => {
- state.count -= num
- return state
- }
- }
- export function dispatch(reducerName, change) {
- // 指定されたReducer
- const reducer = reducers[reducerName]
- // 共通の処理
- state = reducer(state, num)
- hooks.forEach(hook => hook(state))
- }
これが最適解かどうかは微妙ですが、とりあえず共通の処理を一つにまとめることができていますね。
下記は、最終形態です。
- let state = {
- count: 0
- }
- const reducers = {
- increment: (state, num) => {
- state.count += num
- return state
- },
- decrement: (state, num) => {
- state.count -= num
- return state
- }
- }
- export function dispatch(reducerName, change) {
- const reducer = reducers[reducerName]
- state = reducer(state, num)
- hooks.forEach(hook => hook(state))
- }
- let hooks = []
- export function subscribe(hook) {
- hooks.push(hook)
- return () => hooks.splice(hooks.findIndex(x => x === hook), 1)
- }
どうでしょう。これで終わりでしょうか?
いえ…どうやらdispatch
とsubscribe
はどんな状態管理でも同じように使うことができそうです。
同じような状態+振る舞いを持つのならば、それをオブジェクトとして扱えば良さそうです。
では、そういった機構を持つオブジェクトをストア(Store)と名付け、ストアのオブジェクトを作成するファクトリー関数を定義しましょう。
- export function createStore(reducers, initialState) {
- let state = initialState
- return {
- dispatch(reducerName, change) { // 指定されたReducer
- const reducer = reducers[reducerName]
- state = reducer(state, num)
- hooks.forEach(hook => hook(state))
- }
- subscribe(hook) {
- hooks.push(hook)
- return () => hooks.splice(hooks.findIndex(x => x === hook), 1)
- }
- }
- }
あとはこのモジュールを利用して、ストアのオブジェクトを作ることができます。
- import createStore from "???"
- const reducers = {
- increment: (state, num) => {
- state.count += num
- return state
- },
- decrement: (state, num) => {
- state.count -= num
- return state
- }
- }
- const initialState = {
- count: 0
- }
- export default createStore(reducers, initialState)
さて、これをライブラリとして公開したいのですが、名前を何にしましょう。(???に入る名前)
Reducer を主要な概念として採用していることですし…Reduxy とでもしておきましょう。
既視感がある?気のせいですよ。(フフフ)
npm publishっと…。
とまあこんな風に状態管理は難しいものではないんです。
おそらく、途中「こうした方が良いんじゃないか?」と思った方もいらっしゃると思います。
是非色々試して、良いものができたらライブラリとして公開してみてください。
ただ、Reduxなどのライブラリは複数コンポーネントで状態共有をしないなら、使いません。
また、Reactだとポータルの機能もあるので、さらに頻度は減るでしょう。
状態ではなく状態管理のロジックのみを共通利用したい場合は、カスタムフックを作ればよいはずです。
なので、どうしても必要に迫られたときしかReduxを使うことはないのです。
本来使用頻度も高くないということです。
よってReactを難しくしているのがReduxとは到底思えないのです。
また、Redux自体も難しくないことは分かって頂けたんじゃないかと思っていて、おそらくみなさん難しいと思いこんでいるだけです。
その上、実はReactもさほど難しいものではないのです。
どこかで、Reactの簡単さも説明してみたいですね。
React(React.jsのことですよね)に限らず、リアクティブなプログラミングは、綺麗な依存関係を持ったモデルから、そのまま画面に表示すべきものが構築できる、というようなアプリケーションの場合はその力を大いに発揮するのですが、一旦時間的な依存関係があり、例えばそれがサイクルを持つ依存関係をもたらすようなことになると、結局は「フレームワークの機能とやりたいこととのミスマッチを以下にすり合わせるか、ということに手間を取られ、「これってもしかしてフレームワークなしの方が良いんじゃないの?」という疑問を持つことになります。
例えば、ビデオチャットアプリのようなものをReactで書いている時に、「コンピューターで使用できるカメラの一覧」を取得し表示したいとしましょう。単純には、そのページが開かれた時にカメラ一覧という論理的な状態を取得することにし、結果を画面に表示すれば良いように思いますが、その一覧を取得するには非同期的なAPIを使わなくてはなりません。しばらくしてリストが取得できると、Reactはそのリストが変更されたことに反応し、そのページを再描画しようとします。そうするとそのコードの中でまたカメラの一覧を取得するコードが走り、というサイクルになってしまいます。
そのような副作用を処理するために副作用フックという追加的機能を使い、その中には依存関係を破るようなコードを書いて良いわけですが、その入力となっているデータの表現を変えないと、場合によってはその副作用フックが何度もなんども実行されることになります(useCallbackを使うなど)。それでその入力状態の表現を変えるために、その入力を変え、というように結局大きなコード変更が必要となることがあります。
ブラウザー用のフレームワークでは、フレームワークが仮定すること以外をJavaScriptで書けることが普通ですが、それは「leaky abstraction」つまりは抽象化のレイヤーを跨いだプログラミングをしているということであり、事実上問題がなかったとしても「美しくない」ですし、実際にはleaky abstractionはプログラムの複雑性を大きく増加させる元となります。
というあたりが辛いところではないでしょうか。
React Queryをクライアント状態管理にも使うことが個人的にはリーズナブルな方法だと思っています。
React Queryは最適化された非同期状態管理ライブラリの機能を具備しています。[1]
要するに、
- サーバ状態、非同期状態の管理はReact Queryの人気が出てきている。
- アプリにおける主要データがサーバサイドにあり、キャッシュを通じて同期させて扱うようなReactアプリでは、React Queryは極めて優れている。
- React Queryを一番使いこなした状況に持っていくと、サーバ状態以外には大したクライアント状態が残らない。
- その一部の状態だけの保持と管理だけに、異なる状態ライブラリを導入するのは手間も学習コストもかかり、相互運用が必要になりデメリットが大きい。
- React Queryでクライアント状態も管理すると、統合的な最適化が可能で、見通しも良く、React Query Devtoolsも使えてウマー
- 同様の非同期エンジンとしてのRedux Sagaが提供していた高度な機能、リクエストのカスケード発行やタスクキャンセルは、ある程度React Queryで機能を代替できる。
というわけです。ついでに
- export function useQState<T>(key: QueryKey, initial?: T): [T, Dispatch<SetStateAction<T>>] {
- const stateValue = useQuery<T>(key, {
- enabled: false,
- ...((initial !== undefined) ? { initialData: initial } : {})
- }).data as T;
- const queryClient = useQueryClient();
- const stateSetter = (arg: ((arg: T) => void) | T): void => {
- let newValue;
- if (typeof (arg) === 'function') {
- const prevValue = queryClient.getQueryData<T>(key);
- newValue = (arg as any)(prevValue)
- }
- else {
- newValue = arg;
- }
- queryClient.setQueryData<T>(key, newValue);
- }
- return [stateValue, stateSetter];
- }
こんなラッパーフックuseQStateを定義しておけば、useStateと同様の書き味でクライアント状態を使えるようになったりします。例えば
- const [counter, setCounter] = useQState(['counter'], 100);
- :
- <div>count = {counter}</div>
- <button onClick={
- () => setCounter((prevValue) => prevValue + 1)
- }>Increment counter</button>
- <button onClick={
- () => setCounter(5)
- }>SET counter to 5</button>
結果はこんなです。
めっちゃシンプル!
ぜひお試しを。
脚注
Reduxは、Reduxを使わずにいた場合の複雑さが、とても耐えられなくなったときに耐えられるように低減するために使うものです。
Reduxが難しいと感じるなら、Reduxが解決するかもしれない問題がまだそこまで難しくなっていないのでしょう。
あと、スターターキット使うと相当かんたんになってますよ。
どちらでも良いならReact NativeよりはFlutterを選んでいます。
その理由として大きなものを二つ挙げます。
Googleが開発している。
「Android/iOS開発企業自身が提供しているクロスプラットフォーム開発フレームワーク」はFlutterしかありません。
クロスプラットフォーム開発フレームワークはOS公式SDKではない以上、どうしても「政治的、あるいは技術的な理由である日突然使えなくなる」というリスクがあります。そんな最悪の日が訪れてしまった場合でも、Flutterの場合は「iOSはもうだめ。Androidはサポートするよ」とダメージを半減してくれるはずと信じています。さらにFlutterはFuchsiaのUI開発も見据えたフレームワークであるため、Google自身が「もうやーめた」と降りてしまうことはまずないでしょう。
もちろんReact NativeもFacebookが降りる可能性なんてほぼないでしょうけど、両者で比較すればFlutterがリスクが少ないと考えています。
OSのUIに頼らない独自描画である。
Flutterが他と一線を画する大きな特徴として「独自エンジンですべて描画している」点が挙げられます。そのためiOS/Android両方でアプリをリリースしても、完全に同じ見た目、アニメーションにすることが可能です。OS独自の機構やOSSに頼らざるを得ない部分についても、うまくプラグイン開発出来るような仕組みが整備されています。
この特徴は拒否したい方もいると思いますが、私は以前からCocos2d-xのようなゲームエンジンで(ゲームではない)アプリ開発を行った経験もあり、「独自描画」に対しても「最悪自分でなんとかするさ」と考えられる余裕もありました。
フロントエンドの開発やReact、Typescriptなどは学校で習うものではないのではじめはみんな未経験です。
正解です。
まず、前提として、別回答にもありますが、最近のJavaScriptのトレンドでは、バニラJavaScriptのDOM操作APIが昔と比べてかなりマシになっていること、それに、互換性云々うるさかった古いブラウザが消えて、全部モダンにそろってきているので、なるだけ大きなライブラリであるjQueryには依存せず、バニラJavaScriptだけ使う、というトレンドがあります。
別にjQuery使っても構わないんですが、使わずにできるんだったら余計なライブラリに依存する必要がないだろう?というトレンドです。
バニラJavaScriptを選択する (https://www.publickey1.jp/blog/14/javascript_8.html)
【脱jQuery】JavaScriptをネイティブで書くときのあれこれTips
http://youmightnotneedjquery.com/
もうjQueryには頼らない!素のJavaScriptでDOMを操作するための基礎 ... (https://www.webprofessional.jp/dom-manipulation-vanilla-javascript-no-jquery/)
それを踏まえて、質問文については、
DOM操作をするために、
a) バニラJavaScriptもしくはjQueryを活用したDOM操作をすべきか?
b) Reactを含む各種の仮想DOMが使えるJSX(JavaScript拡張Syntax)か?
の二択と解釈します。
アーキテクチャのデザインとすれば、明らかに後者b)のほうが洗練されています。
理由は単純に、DOMっていうものをJavaScriptのファーストクラスオブジェクトとして、扱えるように拡張しているからです。
つまり、
https://www.quora.com/How-can-I-learn-React-JS-from-scratch/answer/Swatee-Chand-2
こういうことで、
Beforeでは、HTML(DOM)は、JSから操作する対象
であったものが、
Afterでは、HTML(DOM)は、JSの値(ファーストクラスオブジェクト)
として統合されます。
具体的には、
- const helloNode = <div>Hello!</div>;
こう書けます。
この原理をきちんと理解さえできれば、DOMなんてものは、ただのJS(X)の値なので、いくらでも自由に操作できます。
単純なWebアプリならば、静的(動的)なHTMLにたいして、適当に外側のJSから操作することが単純だ、と思いがちですが、ちょっと動的に複雑になれば、途端に破綻します。
プログラムの複雑性に耐久性がなく一貫性もなくなるわけですから、ソフトウェアデザインとして堅牢ではない、ということです。
たとえば、よくあるToDoリストを考えましょう。
リストが増えたり減ったりするやつです。この場合、DOM自体が増えたり消えたりするわけで、増えたり、消えたりするDOMに対して、外側のJSからどうやってデータを紐づけたりするんでしょうか?
はい、DOMを足せ、DOMを消せ、っていう命令を出すしかないんですね。非常にやっかいです。
一方で、仮想DOMの場合はどうでしょうか?
DOMってのは単なるJS(X)の値です。まあ普通に考えて、DOMのリストっていうのは、JSの配列(Array)のデータ構造になっているでしょうから、その配列の要素を1個増やしたり減らしたりするだけです。
- const addArray = arr => item =>
- arr.concat(item);
- const removeArray = arr => index =>
- [...arr.slice(0, index),
- ...arr.slice(index + 1)];
たとえば配列listにToDoのデータが入っていて、これをリストタグに展開したいのであれば、Array.map を使って、以下のように展開してやればよいでしょう。
- const newNode = <ul>
- {list.map(item => <li>{item}</li>)}
- </ul>
新しいDOMをこさえるわけですが、こういう新しいDOMを全部ひっくるめる、全部のDOMを構築します。そして最後に古い全部のDOMと新しい全部のDOMをバーっと比較計算して、差分だけ書き換えろ、というJSネイティブのDOMの命令を「裏側」でやらせます。プログラマは関知しません。ただ新しいDOMを構築して投げるだけです。
JSXみたいなJavaScript拡張言語まで発明して、仮想DOMをファーストクラスオブジェクトにして統合した、ってのはFacebookの技術者の驚くべき称賛すべき大発明ではあるわけですが、別にReact自体が最高なわけではありません。
仮想DOMあるいはJSXはとんでもなく素晴らしいですが、具体的な実装のひとつであるReactのAPIは肥大化し、複雑化し、頻繁に破壊的更新をし、使いにくくて仕方がないです。多くのフロントエンドエンジニアがVueへ逃げたのも理解できます。Vueに逃げるのは間違った方向だと思うけれども(同じように複雑性への耐久性がまるでないから)
ゴテゴテしてない純粋な仮想DOMライブラリていうのはそれなりに存在していて、そのひとつが、今ちょっと流行っているhyperappのコアである、
https://github.com/jorgebucaran/superfine
>Minimal view layer for creating declarative web user interfaces
で、仮想DOMだけ、状態管理はしない、というミニマル実装で、非常に素晴らしいです。
しかし、現実的には、状態管理はしないと面倒なことになるわけで、Reactもそれからhyperappも、State受け渡しという仕組みを使ってるんですが、率直に言って劣っています。
関数型プログラミングで状態管理をするには、FRP(関数型リアクティブプログラミング)でやれば良いわけで、自分は、前から、まったく流行っていないミニマルなFRPライブラリ
https://stken2050.github.io/timeline-monad/
を書いて使っていたので、それと組み合わせてパッケージ化しました。
https://stken2050.github.io/unlimitedjs/
正しい実装であると信じていますが、ただ流行っておらず、評価もされていません。
そして、これは別に宣伝のために書いているのではなくて、自分が正しい実装だ、と思うことを書いているだけです、いや本当に笑
0 コメント:
コメントを投稿