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

[Recoil](https://recoiljs.org)を触っていて個人的に気になっているところ、ドキュメントにまだ書かれていない細かいところについて、実験してみた結果を書き起こしておく。こちらは執筆時点でのリリース済み実装（0.0.7）による。

### 非同期Selectorのサブスクリプション

Recoilのselectorは、渡された`get()`関数の中で、`get(state)`で指定した`state`の値をsubscribeできるが、非同期のselectorの場合、非同期処理が発生する前に呼び出されたものでないとsubscribeされない模様。

```typescript
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などに比較変更する手段については提供されているか不明。

##### キャッシュされる例

```typescript
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
```

##### キャッシュされない例

```typescript
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になるわけではない）　。

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