21
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React Recoil事始め

Last updated at Posted at 2020-12-21

はじめに

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が抑制される
  • 使い方が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
21
14
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
21
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?