はじめに
React/React Nativeの新しいState managementとしてRecoil
と言うのがReleaseされています。 Recoilを使って超簡単なアプリを作って、Recoilの使い方をざっくりチラ裏しておきます。
Recoil概要
- 新しいState management Library (
Redux
だったり、GraphQLのApollo
の置き換え) - StateをGlobalに一括管理し、Stateの値を使いたいComponentで利用を宣言すると、Stateの更新がされた時に、そのComponentにも自動でRe-Render処理が走る
- イメージとしては、GlobalにStateを管理し、ReactのContextで必要なComponentにState値を渡すイメージ
- これにより、不要なRe-Renderが抑制され、各種Performanceの恩恵を受ける事が可能
- Stateの
参照
,更新
,Event発火時だけ参照
がMethod的に分けられている- Stateが更新されても、
Stateを更新するだけのComponent
,Button Click時にStateを参照するComponent
等にはRe-Renderが走らない - これにより、不要なRe-Renderが抑制される
- Stateが更新されても、
- 使い方がReact Hookと親和性あり
Recoilサンプル1 (最低限のRecoil機能)
- ボタンをクリックしたら、atomとselectorの値が更新される
- atomとselectorを参照しているcomponentがRe-Renderされる
- このSiteで以下のCodeを実際に動作させる事が可能
import { AppRegistry } from "react-native";
import React, { Component, useMemo, useCallback } from "react";
import { Button, Text, View } from "react-native";
import { RecoilRoot, useRecoilValue, useSetRecoilState, atom, selector } from "recoil";
const clickCountState = atom({
key: "clickCount",
default: 0
});
const clickCountSelector_floorBy5 = selector({
key: "clickCountSelector_floorBy5",
get: ({ get }) => {
const count = get(clickCountState);
return Math.floor(count / 5);
}
});
function TextOne() {
const clickCount = useRecoilValue(clickCountState);
const clickText = useMemo(() => `Click count from atom - ${clickCount}`, [clickCount]);
console.log("TextOne is updated");
return <Text>{clickText}</Text>;
}
function TextTwo() {
const clickCount = useRecoilValue(clickCountSelector_floorBy5);
const clickText = useMemo(() => `Click count from selector - ${clickCount}`,[clickCount]);
console.log("TextTwo is updated");
return <Text>{clickText}</Text>;
}
function ComponentWithRecoil() {
const setClickCountState = useSetRecoilState(clickCountState);
const onPress = useCallback(() => {
setClickCountState((prevValue) => prevValue + 1);
}, [setClickCountState]);
console.log("ComponentWithRecoil is updated");
return (
<>
<TextOne />
<TextTwo />
<View style={{ width: "30%", margin: 10 }}>
<Button title="Click!" onPress={onPress} />
</View>
</>
);
}
class App extends Component {
render() {
return (
<RecoilRoot>
<ComponentWithRecoil />
</RecoilRoot>
);
}
}
AppRegistry.registerComponent("App", () => App);
AppRegistry.runApplication("App", {
rootTag: document.getElementById("root")
});
簡単な解説
1. Stateはatom/selectorで管理される
- atomはStateの最小単位、selectorはatomに何らかの処理を加えた物
- selectorは、Network Call等の非同期処理や、複数のatom/別のselectorの値を合わせた計算結果を管理する
- selectorは、使っている(Subscribeしている)atomが更新されると、自動で更新がかかる
- stateを使う側から見ると、
atom
なのかselector
なのかは意識する必要は無い
- atomを更新すると、関連するselectorが更新され、更新されたatom/selectorを使っているcomponentのRe-Renderが走る
- atomだけでなくselectorの値もSet(更新)が可能 - ここにSample作ってみました
- selectorのSetter関数から渡されたset関数で別のatom/selectorの更新が可能 (その結果、通常のatom更新の処理が行われる)
- (selectorの更新はイマイチUse Caseがピンと来ない...普通はatom更新で済むのだと思います)
2. Component側からはuseRecoilValue()
でStateの値を参照可能
- ここで参照したatom/selectorが更新されると、ComponentにRe-Renderが走る
3. Component側からはuseSetRecoilState()
でStateのSetter関数を取得可能
- Component側から、atom/selectorの値を更新出来る
- useSetRecoilStateはあくまでSetter関数だけなので、(Stateの値自体を参照していない限り)ComponentにはRe-Renderが走らない
4. useRecoilState
は、Stateの値の参照とSetter関数取得を同時に行う
const [value, setValue] = useRecoilState(someAtom)
// は、↓の2行と等しい
const value = useRecoilValue(someAtom);
const setValue = useSetRecoilState(someAtom);
補足
- selectorの値は(現状)memoizeされていない。つまりatomが更新されたがselectorの値が変わらないケースにおいても、selectorを参照しているcomponentにはRe-Renderが走る。これは将来的には直そうとしている模様
-
atomFamily
,selectorFamily
というのもあるが、atom/selectorに引数を渡す機能が追加された物- Arrayのatomに対して、indexを渡すとそのindexの部分を返すような使い方
Recoilサンプル2 (もうちょっとRecoilの機能を使う)
- ボタン1をクリックしたら、atomと非同期処理のselectorが更新される
- 非同期処理のselectorを参照しているcomponentは、loadingの最中はloading indicatorを表示する
- ボタン2をクリックしたら、callbackが発火し、そのTimingでatomを参照してcomponentをRe-Renderする
- このSiteで以下のCodeを実際に動作させる事が可能
import { AppRegistry } from "react-native";
import React, { Component, useMemo, useCallback, useState } from "react";
import { Button, Text, View } from "react-native";
import { RecoilRoot, useRecoilValue, useSetRecoilState, useRecoilCallback, useRecoilValueLoadable, atom, selector } from "recoil";
const clickCountState = atom({
key: "clickCount",
default: 0
});
export const clickCountAsync = selector({
key: "clickCountAsync",
get: async ({ get }) => {
const count = get(clickCountState); // This "get" has to come before async/await line, otherwise will not re-computed when atom value is updated.
await new Promise((resolve) => setTimeout(resolve, 1000));
return count;
}
});
function TextOne() {
const clickCount = useRecoilValue(clickCountState);
const clickText = useMemo(() => `Current click count - ${clickCount}`, [clickCount]);
console.log("TextOne is updated");
return <Text>{clickText}</Text>;
}
function ComponentWithRecoil() {
const setClickCountState = useSetRecoilState(clickCountState);
const onPress = useCallback(() => {setClickCountState((prevValue) => prevValue + 1)}, [setClickCountState]);
console.log("ComponentWithRecoil is updated");
return (
<>
<TextOne />
<View style={{ width: "30%", margin: 10 }}>
<Button title="Click!" onPress={onPress} />
</View>
</>
);
}
function ComponentWithRecoilCallback() {
const [clickCount, setClickCount] = useState(0);
const onPress = useRecoilCallback(({ snapshot }) => async () => {
const clickCount_value = await snapshot.getPromise(clickCountState);
setClickCount(clickCount_value);
});
const clickText = useMemo(() => `Click count update only when button clicked - ${clickCount}`, [clickCount]);
console.log("ComponentWithRecoilCallback is updated");
return (
<>
<Text>{clickText}</Text>
<View style={{ width: "30%", margin: 10 }}>
<Button title="Click to get current count" onPress={onPress} />
</View>
</>
);
}
function ComponentAsyncWithLoadingState() {
const clickCountLoadable = useRecoilValueLoadable(clickCountAsync);
if (clickCountLoadable.state === "hasValue") {
const clickText = `Click count from async selector - ${clickCountLoadable.contents}`;
return <Text>{clickText}</Text>;
} else if (clickCountLoadable.state === "loading") {
return <Text>...loading</Text>;
} else if (clickCountLoadable.state === "hasError") {
return <Text>...error!</Text>;
} else {
return <Text>...something else!</Text>;
}
}
class App extends Component {
render() {
return (
<RecoilRoot>
<ComponentWithRecoil />
<ComponentAsyncWithLoadingState />
<Text> ─────────────────</Text>
<ComponentWithRecoilCallback />
</RecoilRoot>
);
}
}
AppRegistry.registerComponent("App", () => App);
AppRegistry.runApplication("App", {
rootTag: document.getElementById("root")
});
簡単な解説
1. selectorにはasync処理を入れる事が可能
- selectorの中で複数の非同期Selectorの値を処理したい場合には、waitForAllなどを使うと良い
2. useRecoilCallback
はCallbackなどの中でatom/selectorの値を参照時に使う
- User操作などのTimingでComponentの値を更新したい時に使う
- atom/selectorの値が更新されても自動的にはRe-Renderされない
3. useRecoilValueLoadable
は非同期のselectorの値の参照時にLoading Indicatorの表示が可能
- React Suspenseを使えば同様の実装が出来るが、それのSyntax Sugar