recoilで状態管理を行う

2021/07/23

react

recoil

recoilとは

recoilFacebook から2020年5月に発表に発表された状態管理ライブラリ。
公式ページ

セットアップ

npx create-react-app recoil-react-exsample --template typescript
npm install recoil

ルートのコンポーネントを <RecoilRoot> でラップする。

import React from 'react';
import { RecoilRoot } from 'recoil';
import './App.css';

function App() {
  return (
    <RecoilRoot>
      ...
    </RecoilRoot>
  );
}

export default App;

atom

ひとまず配列を取得するatomを宣言する。

export interface todo{
  id: number,
  text: string,
  isComplete: boolean,
};

export type todos = todo[];
import { todos } from '../@types/todo';
import { atom } from 'recoil';

export const todoListState = atom<todos>({
  key: 'todoListState',
  default: [],
});

表示はこのように。

import { todoListState } from '../../atoms/todoLists'
import { useRecoilValue } from 'recoil';

const TodoList = () => {
  const todoList = useRecoilValue(todoListState);

  return (
    <div className="TodoList">
      {
        todoList.map((v,i) => {
          return (
            <div key={i}>
              <p>{v.id}</p>
              <p>{v.text}</p>
              <p>{v.isComplete ? "yes":"no"}</p>
            </div>
          )
        })
      }
    </div>
  );
}

export default TodoList;

変更はatomを通して行う。

import { useState, ChangeEvent } from 'react';
import { useSetRecoilState } from 'recoil';

import { todoListState } from '../../atoms/todoLists'

const TodoListCreator = () => {
  const [inputValue, setInputValue] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: id % 2 == 0
      },
    ]);
    setInputValue('');
  };

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

export default TodoListCreator;

let id = 0;
function getId() {
  return id++;
}

useRecoilValue でatomから値を取得する。
変更する時は useSetRecoilState を使いatomの値を変更する。

selector

atomの値を使い加工したデータを取得する時に使用する。

import { atom, selector } from "recoil";
import { todoListState } from '../atoms/todoLists'
import { todos } from '../@types/todo';

export const todoListFilterState = atom<'Show All' | 'Show Completed' | 'Show Uncompleted'>({
  key: 'todoListFilterState',
  default: 'Show All',
});

export const filteredTodoListState = selector<todos>({
  key: 'filteredTodoListState',
  get: ({get}) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});
import React from 'react';
import { todoListFilterState, filteredTodoListState } from '../../selectors/filterTodoList'
import { useRecoilValue, useRecoilState } from 'recoil';

const TodoListFilter = () => {
  const todoList = useRecoilValue(filteredTodoListState);
  const [filterValue, setFilter] = useRecoilState(todoListFilterState);

  setFilter("Show Completed");

  return (
    <div className="TodoList">
      <p>filterValue:{filterValue}</p>
      {
        todoList.map((v,i) => {
          return (
            <div key={i}>
              <p>{v.id}</p>
              <p>{v.text}</p>
              <p>{v.isComplete ? "yes":"no"}</p>
            </div>
          )
        })
      }
    </div>
  )
};

export default TodoListFilter;

非同期データ

selector を使い非同期的に状態を取得することが出来る。

import { selector } from "recoil";

export const waitText = selector<string>({
  key: 'waitText',
  get: async () => {
    await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve("s");
      },1000)
    });
    return "complate";
  },
});

これを普通に表示するコンポーネントを作成。

import { waitText } from '../../selectors/wait'
import { useRecoilValue } from 'recoil';

const WaitView = () => {
  const text = useRecoilValue(waitText);
  return (
    <div className="WaitView">
      {text}
    </div>
  )
}

export default WaitView;

React.Suspense を使うことで取得前状態にすることが出来る。

<React.Suspense fallback={<div>Loading...</div>}>
  <WaitView/>
</React.Suspense>

終わりに

react hooks らしく書けるので非常に良い。
今回は atomsselectors をフォルダ分けしたけど 一つの状態としてまとめるべきな気がする。
どのように運用して行くかのサンプルはいくつか見たいなぁと思う。