Recoilを触っていて個人的に気になっているところ、ドキュメントにまだ書かれていない細かいところについて、実験してみた結果を書き起こしておく。こちらは執筆時点でのリリース済み実装(0.0.7)による。
非同期Selectorのサブスクリプション
Recoilのselectorは、渡されたget()
関数の中で、get(state)
で指定したstate
の値をsubscribeできるが、非同期のselectorの場合、非同期処理が発生する前に呼び出されたものでないとsubscribeされない模様。
const stateA = atom({
key: "a",
default: 1
});
const stateB = atom({
key: "b",
default: 2
});
// `stateC` は `stateA` の値変更には反応するが、`stateB`の変更には反応しない
const stateC = selector({
key: "c",
async get({ get }) {
const a = get(stateA);
await delay(1000);
const b = get(stateB);
return a * b;
}
});
//
const [a, setA] = useRecoilState(stateA);
const [b, setB] = useRecoilState(stateB);
const c = useRecoilValueLoadable(stateC);
// 初回描画
console.log("a: ", a); // => a: 1
console.log("b: ", b); // => b: 2
console.log("c: ", c.state, c.contents); // => c: loading <Promise>
// + 1 sec
console.log("a: ", a); // => a: 1
console.log("b: ", b); // => b: 2
console.log("c: ", c.state, c.contents); // => c: hasValue 2
// Aの値を変更
setA(2)
console.log("a: ", a); // => a: 2
console.log("b: ", b); // => b: 2
console.log("c: ", c.state, c.contents); // => c: loading <Promise>
// + 1 sec
console.log("a: ", a); // => a: 2
console.log("b: ", b); // => b: 2
console.log("c: ", c.state, c.contents); // => c: hasValue 4
// Bの値を変更
setB(3)
console.log("a: ", a); // => a: 2
console.log("b: ", b); // => b: 3
console.log("c: ", c.state, c.contents); // => c: hasValue 4 (←更新されない)
Selectorのキャッシュ
Recoilのselectorは計算結果をキャッシュ(memoize)するが、比較は厳密等価比較なので注意。メモ化をカスタマイズする方法、たとえばshallow equalなどに比較変更する手段については提供されているか不明。
キャッシュされる例
const stateA = atom({
key: "a",
default: 1
});
const stateB = selector({
key: "b",
get({ get }) {
const a = get(stateA);
const b = Math.floor(a / 10);
console.log("calc B = ", b);
return b;
}
});
const stateC = selector({
key: "c",
get({ get }) {
const b = get(stateB);
const c = b * 2;
console.log("calc C = ", c);
return c;
}
});
//
const [a, setA] = useRecoilState(stateA);
setA(2);
// => calc B = 0
setA(3);
// => calc B = 0
setA(10);
// => calc B = 1
// => calc C = 2
キャッシュされない例
const stateA = atom({
key: "a",
default: 1
});
const stateB = selector({
key: "b",
get({ get }) {
const a = get(stateA);
const b = Math.floor(a / 10);
console.log("calc B = ", b);
return { num: b };
}
});
const stateC = selector({
key: "c",
get({ get }) {
const { num: b } = get(stateB);
const c = b * 2;
console.log("calc C = ", c);
return c;
}
});
//
const [a, setA] = useRecoilState(stateA);
setA(2);
// => calc B = 0
// => calc C = 0
setA(3);
// => calc B = 0
// => calc C = 0
setA(10);
// => calc B = 1
// => calc C = 2
多段の非同期Selector
非同期Selectorを多段につなげた場合、最前線のselectorの計算が始まったタイミングから依存するすべてのstateがloading
状態になる(直接subscribeしているstateの値が更新されたタイミングでloadingになるわけではない) 。
const stateA = atom({
key: "a",
default: 1
});
const stateB = selector({
key: "b",
async get({ get }) {
const a = get(stateA);
await delay(1000);
return a * 2 + 1;
}
});
const stateC = selector({
key: "c",
async get({ get }) {
const b = get(stateB);
await delay(1000);
return b * b;
}
});
// b, c は非同期selectorなので`useRecoilValueLoadable`でロード中の状態を取れるようにする
const [a, setA] = useRecoilState(stateA);
const b = useRecoilValueLoadable(stateB);
const c = useRecoilValueLoadable(stateC);
// 初回描画時
console.log("a: ", a); // => a: 1
console.log("b: ", b.state, b.contents); // => b: loading <Promise>
console.log("c: ", c.state, c.contents); // => c: loading <Promise>
// + 1 sec
console.log("a: ", a); // => a: 1
console.log("b: ", b.state, b.contents); // => b: hasValue 3
console.log("c: ", c.state, c.contents); // => c: loading <Promise>
// + 1 sec
console.log("a: ", a); // => a: 1
console.log("b: ", b.state, b.contents); // => b: hasValue 3
console.log("c: ", c.state, c.contents); // => c: hasValue 9
//
setA(2);
console.log("a: ", a); // => a: 2
console.log("b: ", b.state, b.contents); // => b: loading <Promise>
console.log("c: ", c.state, c.contents); // => c: loading <Promise>
// + 1 sec
console.log("a: ", a); // => a: 2
console.log("b: ", b.state, b.contents); // => b: hasValue 5
console.log("c: ", c.state, c.contents); // => c: loading <Promise>
// + 1 sec
console.log("a: ", a); // => a: 2
console.log("b: ", b.state, b.contents); // => b: hasValue 5
console.log("c: ", c.state, c.contents); // => c: hasValue 25
なお非同期selectorを3段 (stateA => stateB => stateC => stateD) にしたらCannot add node 1 because a node with that id is already in the Store.
というエラーが出た。やはり非同期周りは特に不安定っぽい印象。