状態管理にはEffector!Effectorの使い方解説
はじめに#
iamfax.comではフロントエンドではnextjsを使っていますが、状態管理にはeffectorというライブラリを使っています。
状態管理はuseStateを使えばいいじゃないか!と思うかもしれませんが、 入力フィールドが増えたり、バリデーション、エラーハンドリング、ローディング状態など、Formが持つべき状態が増加すると、useStateの数がどんどん増えていきます。各状態がコンポーネント内に散らばり、関連性が分かりにくく、管理が煩雑になります。
大規模・複雑なアプリケーションではzustand, jotai, Redux などの検討をする人がいると思いますが、是非effectorを候補に入れてみてください。
effectorは不思議なほど日本で認知度が低く、日本語の説明サイトは皆無となっています。そこで今回は簡単なeffectorの使い方を解説し、effectorの普及に貢献していけたらと思います。
effector#
effectorは学習コストが高いと言われますが、実際はそこまででは無いと思います。主要な関数は4つくらいで、これらの概念さえつかめば使えてくるはずです。その4つとは、
- createEvent, createStore, createEffect, sample
それぞれの使い方を説明していきます。
createEvent, createStore関数について#
例によってカウンターアプリを考えてみましょう。早速ですが以下のコードをカウンターの実装例です。
import { createEvent, createStore, combine } from "effector";
const plus = createEvent();
const storeCounter = createStore(1);
storeCounter.on(plus, (count) => count + 1);
const MyCounter = () => {
...
}
effectorでは、状態の保存先のstoreオブジェクトと、それを変えるeventオブジェクトを作る必要が有り、それぞれはcreateEvent, createStore
で作ります。
なお、createStore
などは、コンポーネントの外に書く必要が有ります。(間違えてMyCounterの中に書かないように)。
作成したeventオブジェクトをstoreオブジェクト紐付けることで、例えばボタンクリックの際にeventオブジェクトを呼べば、storeオブジェクトの値が変わるようになります。
ReactのuseStateと比較すると、const [counter, setCounter] = useState()
を定義後、handleClickみたいなのを宣言して、その中でsetCounter(counter + 1)
というコードを書いていました。
しかしながらeffectorの場合はインクリメントする関数は最初に宣言時に定義します。私にはこちらのほうがより直感的な気がします。
storeオブジェクトを参照する時は、useUnit
を使います。
import { useUnit } from "effector-react";
... // createEvent, createStoreの定義
const MyCounter = () => {
...
const value = useUnit(storeCounter) // storeCounterは先程の例で定義したもの
return (
<div>
<button onClick={plus}>+</button>
<div>{value}</div>
</div>
)
}
onClickで直接Eventオブジェクトのplusを指定します。useUnit
はuseStoreの値を取得するという認識で大丈夫です。
同じようなコードは公式にもあります。参考。
createEffect, sample 関数について#
fetchなどのAPI通信処理や非同期的な計算処理などはcreateEffect
を使って実現します。名前がuseEffectと似ていますが、違う役割なので混同しないようにしてください。
import { createEffect, createStore, sample } from "effector";
const url = "https://example.com/api"
const fetchUserFx = createEffect((url) => fetch(url).then((req) => req.json()));
const userNameStore = createStore("Anonymous"); // anonymous で初期化
const loadUserEvent = createEvent();
sample({
clock: loadUserEvent,
fn: () => url,
target: fetchUserFx,
});
userNameStore.on(fetchUserFx.doneData, (_, user) => user.username);
const UserExample = () => {
...
const userName = useUnit(userNameStore)
return (
<div>
<button onClick={loadUserEvent}>Load User</button>
<div>{userName}</div>
</div>
)
}
createEffect
では、urlを受取りfetchする関数が定義されています。
sample
という関数がありますが、eventとeffectoオブジェクトを紐付けています。
sample
関数ではclockで監視するイベントを監視し、発火すると、targetで指定したEffectオブジェクトに値を渡します。
.on
というという書き方をしてますが、これがevent監視のようなもので、fetchが終わった事後処理を書けたりします。
.on(...).on(...)
というふうにチェーン的に書け、Eventを登録するのです。
.onの他にも値をリセットする .reset
や変更検知の.watch
なんかもありますが、徐々に知っていきましょう。
先の例ではurl固定でした。 実際のシーンではstoreオブジェクトを指定して渡します。
const someEvent = createEvent(); // event 登録
const htmlStore = createStore("foo"); // storeの作成
const faxNumberStore = createStore(0); // storeの作成
const debuggingFX = createEffect( // effect登録。変数を2つ、受け取る例
async (params: {
html: string;
faxNumber: number;
}) => {
console.log(
"debugging",
params.faxNumber,
params.html,
);
// you can do something here
}
);
const sendFaxFx = createEffect( ... ); // effect 登録
sample({
clock: someEvent, // 発火イベント
source: {
html: htmlStore, // store オブジェクトを渡す
faxNumber: faxNumberStore, // storeObject
},
target: [sendFaxFx, debuggingFX], // ターゲットは2つ以上登録可能
});
const FaxExample = () => {
....
}
変数のsourceにはcreateStore
で作ったものをstoreオブジェクトを指定しています。(useUnitする必要はないので注意ください)
targetも複数指定可能です。effectオブジェクトのdebuggingFxの引数を見てわかるとおり、sampleからは実体化した値が渡されることになります。
繰り返しになりますが、sample
やcreateEffect
も関数の外、コンポーネントの外部で行う事に注意してください
sampleをコンポーネント内部で扱いたい時#
sample
やcreateEvent
やcreateEffect
を、コンポーネント内で書きたい時はどうするのでしょうか。
その時はreactのuseMemo
かuseEffect
を使います。
useMemo
を使う時の例です。コンポーネント内で呼び出したいときなどはmemo化します。
import { createEffect, createStore, sample } from "effector";
import { useMemo } from "react";
const somePage = () => {
// formEvent を使いたい時。関数化
const formEvent = useMemo(() => {
const _formEvent = createEvent(); // should define it in here.
const sendFaxFx = createEffect(
(params: {
html: string;
faxNumber: number;
}) => {
fetch( ..... );
}
);
sample({
clock: _formEvent,
source: {
html: editorHtmlStore,
faxNumber: faxFormStore,
},
target: sendFaxFx,
// target: [debuggingFX],
});
return _formEvent;
}, [someDependency]
);
....
formEvent() // formEventを呼びたい
}
一方で、eventオブジェクト経由で呼ぶときなど、そのEventオブジェクトと紐付けたいだけの場合useEffect
で定義できます
import {
...
clearNode, // clearNodeをインポートする
} from "effector";
....
const somePage = () => {
useEffect(() => {
const sendFormFx = createEffect((params: UserForm) => {
fetch( ....);
});
const target = sample({
clock: someEvent,
source: someStore,
target: sendFormFx,
});
return () => {
clearNode(target); // ここでunbindをわすれないこと!
};
}, []);
}
useEffect
で使う時は必ずclearNode
を忘れないようにしてください。
そうしないと、何度もsampleがバインドされてしまうことになってしまい、例えばsomeEventを1度呼んだだけなのに、何度もsendFormFxが呼ばれるようになったりしてしまいます。
useStoreMap, useListについて(便利関数)#
最後は便利関数を紹介します。これは重要度は低いのですが、知っておくと便利だと思います。
useStoreMap
は、Storeオブジェクトから特定のキーに対応する値を効率的に取り出したい時に使う便利関数です。
関数型のMapのようなアプローチに似ています。面白い特徴として、リターンバリューにより型が決まります。
つまり、stringを返せばstring, stringのリストを返せばstring[], ReactNodeを返すreturn ()みたいなこともできます。
id = "somename"
const value = useStoreMap({
store: storeUsers, // storeUsers = createStore({...})
keys: [id],
fn: (user) => {
if (id === "0x0000") {
return user[id] ?? "";
} else {
// エラー処理
return ""; // 例えばデフォルト値を返すなど
}
},
}); // storeの型を指定
useList
はReactNodeのリストを返すものです。定義は次のようになっています。
useList(
$store: Store<T[]>,
fn: (value: T, index: number) => React.ReactNode,
): React.ReactNode;
具体的にはここ参考にしてみてください。
useList, useStoreMap
とも便利関数なので特に理解する必要はありません。
最後に#
簡単にですが、Effectorライブラリの使い方を説明しました。
effector
は周りでも私ともう一人のエンジニアしか使っている人を日本で見たことはありません。
Githubでも4.7Kついているくらいなので外国籍の人はいますが日本人はX上でもいません。日本語マニュアルが少ないというか、存在していない事に驚いています。
それはおそらく少し学習コストが高いと感じることが原因なのかもしれません。
今回はそんな学習コストは高くないという説明が伝われば幸いです。
使っていくとuseState
やReduxの嫌だった部分とかを解消してくれる、間違いなくいい状態管理ライブラリであることに気づくはずですので是非使いましょう、effector!