独立した2つのatomを同期する
2つのatomを受け取り、実装を変えた2つのatomを返す案
aAtomとbAtomに対し、aAtom'とbAtom'を返す
aAtom'とbAtom'は通常のatomと同様のinterfaceで使える
例えば、aAtom'を更新すると、同時にbAtomも更新される
従って、
同期したい場合は'の方を使用し、
同期したくない場合は、前者の方を使用すれば良い
全く同じ見た目になるので、意図せずもう片方も同期されていたとならないように注意が必要
この辺の要件を満たせる
好きな相手と同期できる
aが、bやcと同期も可能
a単体でのupdateも可能
通常のatomと同じinterfaceを持つ
subscribeする案
a->b
a<-b
の2つを用意する
しかし、書き方によってはa,bのどちらかが変わると両方更新されてしまう
実際は、aが変わったらbを更新する、bが変わったらaを更新するとしたい
比較のためなどに、両方をgetすることができない
同期用のセレクタを用意する
code:ts
const syncAtom = atom(
(get) => get(atomA),
(get, set, update) => {
set(atomA, update);
set(atomB, update);
}
);
このセレクタを使って更新すれば両方とも更新される
fetchした結果を保持するatom等があるときつい
後に更新された方を優先する、という風にどうにかして書けないかな
counterとかも使っていいから
--
2つのatomを監視する
どちらかが更新される
どちらが更新されたのかを特定する
これをやる手段がない?
いけたわmrsekut.icon
そっちを優先してもう片方を上書きする
要件
無限ループを避ける
hooksまでいかずにatomレベルで完結したい
sync(a,b)ぐらいにかけると良い
イメージこんな感じ
わけあって使ってないのでテストしてない
code:ts
import { type WritableAtom, atom } from 'jotai';
import { atomEffect } from 'jotai-effect';
import isEqual from 'react-fast-compare';
export const syncAtoms = <T>(
aAtom: WritableAtom<T, T, void>, bAtom: WritableAtom<T, T, void>, ) => {
const prevAtom = atom<T | 'init'>('init'); // symbolとか
return atomEffect((get, set) => {
const a = get(aAtom);
const b = get(bAtom);
const prev = get(prevAtom);
const updatedA = !isEqual(prev, a);
const updatedB = !isEqual(prev, b);
switch (true) {
// ① どちらも更新されていない
case !updatedA && !updatedB: {
return;
}
// ② aが更新された
case updatedA && !updatedB: {
set(bAtom, a);
set(prevAtom, a);
return;
}
// ③ bが更新された
case !updatedA && updatedB: {
set(aAtom, b);
set(prevAtom, b);
return;
}
// ④ どちらも更新された
case updatedA && updatedB: {
/**
* TODO: 両方更新されたときの扱い
* e.g.
* - a (第1引数)を常に優先する
* - 別の引数で優先する方を指定できるようにする
* - 両方更新する、両方更新しない
* - 例外を投げる
*/
return;
}
default:
break; // cannot reach here
}
});
};
code:ts
import { type WritableAtom } from 'jotai';
import { atomEffect } from 'jotai-effect';
type Converter<I, E> = (v: I) => Result<E>;
export const syncWithConverterAtom = <A, B>(
atomA: WritableAtom<A, A, void>, atomB: WritableAtom<B, B, void>, ) => {
return atomEffect((get, set) => {
const resultA = toB(get(atomA));
const resultB = toA(get(atomB));
if (resultA.ok && resultB.ok) {
set(atomB, resultA.data);
set(atomA, resultB.data);
}
});
};
type Result<T> = { ok: true; data: T } | { ok: false; error: unknown };
export const ok = <T>(v: T) => ({ ok: true, data: v }) as const;
export const ng = (v: unknown) => ({ ok: false, error: v }) as const;
かなり雑だけど
これを上記のsyncAtomと一般化できそう
syncAtom = syncWithConver id,idみたいにもできるけど、
そうすると、後者の責務が増えすぎるかも
切り出せるところはもっと細かく要件毎に関数に切り出せそう
todos
name
resultの取り扱い
syncAtom同様に、どちらを優先するかの機構
など
2種類のatomを受け取り、2種類のsetterを返す案
同期したいときはそのsetterで更新する
同期したくないときは通常のsetterで更新すれば良い
同期する場合は専用のsetterをimportしてこないといけない
atomの定義とsetterの生成を同じ箇所でやってないと可読性が落ちる