ぜんぜんReactのことよくわかっていないので勉強がてらまとめていったメモです。全然違うぞこの野郎!と思ったらこっそり教えてください。
ZMMという解説動画メーカーのフロントエンドを書いているけどReactなにもわかっていないので調べながら書いているというステートです。
React 概要
ブラウザで動作するSPAアプリケーションを作成するときは、UIライブラリとしてReactがよく使われる。
ReactではUIの各要素をコンポーネントというコード上の構成要素に分解し、それぞれのコンポーネントがJSX構文を使ってDOM的なもの(実際のDOMではなく、パフォーマンス上の理由でVDOMという概念を使っている)を返すことでコンポーネントを表現している。コンポーネントに分解することで、メンテナブルで再利用しやすいというのが売りである。
コンポーネントの記述方法として関数コンポーネントとクラスコンポーネントの2種類が用意されているが、最近は関数コンポーネントが優勢である。 関数コンポーネントは、コンポーネントはVDOMを返す純粋な関数として表現される。
function Hello() { return <span>Hello!</span> }
また、コンポーネントの中でさらに別のコンポーネントを呼び出すことができる。大文字から始まる関数はコンポーネントとみなされる。
function TripleHello() { return <><Hello /><Hello /><Hello /></> }
Reactにコンポーネントとその描画先となる実際のDOMを渡すと、そのコンポーネントがレンダされる。
const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<TripleHello />);
コンポーネントは階層的である
レンダするためにReactに渡せるコンポーネントは同時に1つである。したがって一般的には、App
と呼ばれる最上位コンポーネントの中でさらに別のコンポーネントを呼び出すという階層構造によってアプリケーションが構成される。
データフロー
上位のコンポーネントは下位のコンポーネントにprops
を通じてデータを渡すことができる。関数コンポーネントでは、propsとは単なる引数である。
type HelloProps = { name: string } function Hello(props: HelloProps) { return <span>Hello, {props.name}!</span> }
function App() { return <Hello name="windymelt" /> }
データフローの固定
Reactはデータフローを一方向に固定している。すなわち、コンポーネントの階層構造の頂点から下流へとpropsが流れていき、決して逆流しない。渡ってきたpropsを変更することは許可されない。
useState
その一方で、コンポーネントが独自の状態を持つための仕組みが存在する。関数コンポーネントでは純粋な関数を定義しなければならないので、useState
という仕組みでこれを解消している。
useState
は端的にはReactに再レンダーを要求するための仕組みであり、状態を持ちつつも関数自体を純粋に保つことができる:
import { useState } from 'react'; type CounterProps = { count: number } function Counter(props: CounterProps) { const [count, setCount] = useState<number>(props.count); const onClick = () => setCount((c) => c+1); return <button onClick={onClick}>{count}</button> }
最初にuseState
が呼び出されると、Reactランタイムは内部的にコンポーネントに状態テーブル(のようなもの)を作成し、引数で初期化する。更新用関数であるsetFooBar
が呼び出されると、状態テーブル(のようなもの)を更新し、必要な箇所をレンダーし直す。
極端な話、setFooBar
で状態を変更したときは全コンポーネントをレンダーし直せば状態を画面に反映させることはできる。だがそれだとパフォーマンス上の問題が当然発生するため、useState
によって状態とコンポーネントとの関係をReactランタイムが管理し、setFooBar
によって状態を変更したときはレンダーし直すべきコンポーネントをReactが決定し、そこだけDOMを更新することでパフォーマンスを稼いでいる。
Reactのランタイムに何かをお願いするような似たような機能をまとめてHooksと呼ぶ。useState
もフックである。一種のシステムコールだと考えてよいかもしれない。メモリ管理をOSにやってもらうように、コンポーネントの状態管理をReactにやってもらうのである。クラスコンポーネントでは自前でやっていたことを、関数コンポーネントでは手放してランタイムに押し付けている。そのかわりコンポーネントは純粋関数になって扱いやすくなった。
データフローの逆流
さて、Reactがデータフローを上意下達に固定している一方、コンポーネントは状態を持てることがわかった。では2つのコンポーネントが互いの状態に影響を及ぼしうるような場合はどうすればいいのか?という疑問が湧き起こる。
もちろん、コンポーネント同士が互いのStateに勝手に干渉してはいけない。
Reactは、こうした問題は直近の親コンポーネントに状態を管理させよと教えている。実際に状態を使うコンポーネントは、状態変数と更新用のsetFooBar
をpropsとして受け取り、状態を変化させたい場合はこれを使って共通の状態を変化させる。
React での state の共有は、state を、それを必要とするコンポーネントすべての直近の共通祖先コンポーネントに移動することによって実現します。これを “state のリフトアップ (lifting state up)” と呼びます state のリフトアップ – React
このようにすることで、最小限の範囲で状態を共有できるようになる。
大きな泥団子
ところで自分は一種のXMLエディタを作成しようとしている。これって結局1つの大きな状態を全体で共有しているだけなのではないか?アプリケーションの形態によっては、どうしてもこうなってしまうおそれがある。
そのような場合には、結局最上位のApp
コンポーネントに巨大な状態定義が置かれ、それを切り分けて操作するようなstate地獄になるのではないか?
お前はどうせ大きな泥団子なんだよ
というわけでグローバルステートライブラリとでもいうべきものが発明された。複雑なアプリケーションでは状態がどうせ泥団子になってしまうので、巨大な1つの状態をReactの管理から剥がして隔離し、React側からはそれを読み書きするだけにするというコンセプトである。
グローバルステートライブラリには、Redux、Recoil、Jotaiなどが存在する。エディタ的なアプリケーションでは、こういったライブラリを使ったほうが筋良くアプリケーションを組めるのではないかと思う。
まとめ
エディタ書くときはReduxとか入れたほうがいいことを学んだ・・・。