8
3

More than 3 years have passed since last update.

Recoil の振る舞いについて、幾つかの実験とその結果

Posted at

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.というエラーが出た。やはり非同期周りは特に不安定っぽい印象。

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3