Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Reactの状態管理ライブラリについて調べた記録

ぜんぜんReactのことよくわかっていないので勉強がてらまとめていったメモです。全然違うぞこの野郎!と思ったらこっそり教えてください。

ZMMという解説動画メーカーのフロントエンドを書いているけどReactなにもわかっていないので調べながら書いているというステートです。

blog.3qe.us

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を更新することでパフォーマンスを稼いでいる。

rajatexplains.com

dev.to

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.js.org

recoiljs.org

jotai.org

まとめ

エディタ書くときはReduxとか入れたほうがいいことを学んだ・・・。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?