【React / Next】Recoilを隠蔽して、Flux pattern風に使う

Recoil を隠蔽して使いたかったので、その個人的な備忘録です。

# やりたいこと
- Recoil をカプセル化して、Flux patternのように Action / Getter を作りたい
- 全レイヤーが Recoil を直接 import しないようにしたい
- サンプルとして jsonplaceholder から fetch
  - https://jsonplaceholder.typicode.com/todos

# 環境
- React: 17.0.2
- Next: 11.1.3
- Recoil: ^0.7.3-alpha.2

# サンプルコード

## src/store/todoList/types.ts
```typescript:src/store/todoList/types.ts
// 本来は domain層で定義
export type Todo = {
  id: number;
  title: string;
  userId: number;
  completed: false;
};

/** State の型 */
export type TodoListState = {
  todoList: Todo[];
};

/** Getters の型 */
export type TodoListGetters = {
  useTodoList: () => TodoListState;
};

/** Actions の型 */
export type TodoListActions = {
  useFetchTodoList: () => {
    fetchTodoList: () => void;
  };
  useSetTodo: () => {
    setTodo: (todo: Todo) => void;
  };
};
```

## src/store/todoList/main.ts
```typescript:src/store/todoList/main.ts
import { useCallback } from 'react';
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';
import recoilUseCase from '@/application/recoilTest';
import { Todo, TodoListActions, TodoListGetters, TodoListState } from './types';
import { RECOIL_ATOMS_KEYS } from '../keys';

// state はそのまま export しない
const todoListState = atom<TodoListState>({
  key: RECOIL_ATOMS_KEYS.TODO_LIST_STATE,
  default: {
    todoList: [],
  },
});

// Getter的役割
const useTodoList = () => {
  return useRecoilValue(todoListState);
};

export const todoListGetters: TodoListGetters = {
  useTodoList,
};

// Action をカスタムフックとして定義
/**  todoList の fetch */
const useFetchTodoList = () => {
  const setState = useSetRecoilState(todoListState);
  const { data } = recoilUseCase.fetch();

  const fetchTodoList = useCallback(() => {
    setState(() => {
      if (!data) {
        return {
          todoList: [],
        };
      }
      return {
        todoList: data,
      };
    });
  }, [data, setState]);

  // TodoListState の保持している state が複数ある場合
  // const fetchTodoList = useCallback(() => {
  //   setState((prev) => {
  //     if (!data) {
  //       return {
  //         todoList: [],
  //       };
  //     }
  //     return {
  //       ...prev,
  //       todoList: data,
  //     };
  //   });
  // }, [data, setState]);

  return { fetchTodoList };
};

/** Todoを追加 */
const useSetTodo = () => {
  const setState = useSetRecoilState(todoListState);

  // 引数がある場合は、useCallbackの第一引数へ記述
  const setTodo = useCallback(
    (todo: Todo) => {
      setState((prev) => {
        return {
          todoList: [...prev.todoList, todo],
        };
      });
    },
    [setState],
  );

  return { setTodo };
};

export const todoListActions: TodoListActions = {
  useFetchTodoList,
  useSetTodo,
};
```

## src/pages/recoil/index.tsx
```tsx:src/pages/recoil/index.tsx
import { todoListActions } from '@/store/testTodoList';
import RecoilTestTemplate from '@/components/templates/recoilTest';

const RecoilPage = () => {
  // pages フォルダで fetch だけする
  const { fetchTodoList } = todoListActions.useFetchTodoList();
  fetchTodoList();

  return <RecoilTestTemplate />;
};

export default RecoilPage;
```

## src/components/recoilTest.tsx
```tsx:src/components/recoilTest.tsx
import { todoListActions, todoListGetters } from '@/store/testTodoList';
// 本来は domain層
import { Todo } from '@/store/testTodoList/types';

export const RecoilTestTemplate = () => {
  // Action
  const { setTodo } = todoListActions.useSetTodo();
  // Getter
  const todoListState = todoListGetters.useTodoList();
  const [inputData, setInputData] = useState<string>();

  const handleChange = (e: string) => {
    setInputData(e);
  };

  const handleClickButton = () => {
    const newTodo: Todo = {
      userId: 500,
      id: 500,
      title: inputData ? inputData : '',
      completed: false,
    };
    setTodo(newTodo);
  };

  return (
    <>
      {todoListState.todoList &&
        todoListState.todoList.map((item: any) => <p key={item.id}>{item.title}</p>)}
      <div>
        <input type="text" onChange={(e) => handleChange(e.target.value)} />
        <button onClick={handleClickButton}>add</button>
      </div>
    </>
  );
};
```