LoginSignup
16
14

More than 1 year has passed since last update.

おんなじTODOアプリをuseState / useReducer / useContext / Redux / Recoil を使って実装してみた

Last updated at Posted at 2022-02-02

つくるもの

ほぼチュートリアル通りのあいつです。

Image from Gyazo

要件は以下のような感じです。
・タスクを登録することができる
・左チェックボックスで完了にすることができる
・右の削除ボタンで削除ができる
・チェックしたタスクが完了したタスクに表示される
・チェックしていないタスクが現在のタスクに表示される
・削除したタスクが削除済みのタスクに表示される
・削除したタスクを復元することができる
・削除済みのリストで「ゴミ箱を空にする」を押すと削除済みタスクが抹消される

管理されるStateとStateを更新する関数

それぞれ以下を管理する方法が違う。

管理されるState

  • セレクトボックスの入力値
  • タスク作成フォームの入力値
  • タスク一覧
  • タスクの中身
    • タスクのvalue
    • タスクのcheckboxの真偽値
    • 削除済みのタスクかどうかの真偽値

Stateを更新する関数

  • セレクトボックスの入力値を更新し、値によってフィルタしたタスクのリストを返す関数
  • タスク作成フォームの入力値を更新する関数
  • タスクの一覧を更新する関数
  • タスクのチェックボックスの値を更新する関数
  • タスクの削除フラグを更新する関数
  • 削除済みのタスクを一括削除する関数

useStateを使った実装

Todoのリスト / セレクトボックスの入力値 / フォームの入力値 をuseStateで管理している。

const [state, setState] = useState<>('初期値');

setState('新たな値');
App.tsx
import React, { useState } from 'react';
import './App.css';

interface Todo {
  value: string,
  id: number,
  checked: boolean,
  removed: boolean;
}

type Filter = 'all' | 'checked' | 'unchecked' | 'removed';

const App: React.VFC = () => {
  const [text, setText] = useState<string>('');
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<Filter>('all');
  const handleOnClick = () => {
    if(!text) return;
    const newTodo: Todo = {
      value: text,
      id: new Date().getTime(),
      checked: false,
      removed: false,
    };

    setTodos([newTodo, ...todos]);
    setText('');
  };

  const handleOnEdit = (id: number, value: string) => {
    const newTodos = todos.map((todo) => {
      if (todo.id === id) {
        todo.value = value;
      }
      return todo;
    });

    setTodos(newTodos);
  };

  const handleOnCheck = (id: number, checked: boolean) => {
    const newTodos = todos.map((todo) => {
      if (todo.id === id) {
        todo.checked = !checked;
      }
      return todo;
    });

    setTodos(newTodos);
  };

  const handleOnRemove = (id: number, removed: boolean) => {
    console.log('removed: ', removed);
    console.log('id: ', id);
    const newTodos = todos.map((todo) => {
      if (todo.id === id) {
        todo.removed = !removed;
      }
      return todo;
    });

    setTodos(newTodos);
  };

  const filteredTodos = todos.filter((todo) => {
    switch (filter) {
      case 'all':
        return !todo.removed;
      case 'checked':
        return todo.checked && !todo.removed;
      case 'unchecked':
        return !todo.checked && !todo.removed;
      case 'removed':
        return todo.removed;
      default:
        return todo;
    }
  });

  const handleOnEmpty = () => {
    const newTodos = todos.filter((todo) => !todo.removed);
    setTodos(newTodos);
  };

  return (
    <div>
      <select
        defaultValue="all"
        onChange={(e) => setFilter(e.target.value as Filter)}>
        <option value="all">すべてのタスク</option>
        <option value="checked">完了したタスク</option>
        <option value="unchecked">未完了のタスク</option>
        <option value="removed">削除済みのタスク</option>
      </select>
      {
        filter === 'removed' ? (
          <button onClick={() => handleOnEmpty()}>ゴミ箱を空にする</button>
        ) : (
          <form onSubmit={(e) => e.preventDefault()}>
          <input
            type="text"
            value={text}
            disabled={filter === 'checked'}
            onChange={(e) => setText(e.target.value)}
          />
          <button
            onClick={() => handleOnClick()}
            disabled={filter === 'checked'}
          >追加</button>
        </form>
        )
      }
      <div className="align-lists">
        <ul>
          {filteredTodos.map((todo) => {
            return (
              <li key={todo.id}>
                <input
                  type="checkbox"
                  disabled={todo.removed}
                  checked={todo.checked}
                  onChange={() => handleOnCheck(todo.id, todo.checked)}
                />
                <input
                  type="text"
                  disabled={todo.checked || todo.removed}
                  value={todo.value}
                  onChange={(e) => handleOnEdit(todo.id, e.target.value)}
                />
                <button onClick={() => handleOnRemove(todo.id, todo.removed)}>
                  {todo.removed ? '復元' : '削除'}
                </button>
              </li>
            )
          })}
        </ul>
      </div>
    </div>
  );
};

export default App;

useReducerを使った実装

Todoのリスト / セレクトボックスの入力値 / フォームの入力値 を state にまとめて管理している。
更新はreducerメソッドをdispatchで呼び出して行う。
アクションをdispatchすると、reducer に現在のstateとアクションが渡って処理される。
stateを更新するロジックは reducer に集約される。

const [state, dispatch] = useReducer(reducer, '初期値');

dispatch({ type: 'アクションのタイプ', ...その他更新に使用する値})

const reducer = (state, action) => {
  switch (action.type) {
    case 'hoge':
      ...
  }
}

App.tsx
import React, { useReducer, memo, Dispatch } from 'react';
import './App.css';

interface Todo {
  value: string,
  id: number,
  checked: boolean,
  removed: boolean;
}

type Filter = 'all' | 'checked' | 'unchecked' | 'removed';

interface State {
  text: string;
  todos: Todo[];
  filter: Filter;
}

const initialState: State = {
  text: '',
  todos: [],
  filter: 'all',
};

type Action = 
  { type: 'change', value: string }
  | { type: 'filter', value: Filter }
  | { type: 'submit' }
  | { type: 'empty' }
  | { type: 'edit', id: number, value: string }
  | { type: 'check', id: number, checked: boolean }
  | { type: 'remove', id: number, removed: boolean }

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'change': {
      return { ...state, text: action.value };
    }
    case 'submit': {
      if (!state.text) return state;

      const newTodo: Todo = {
        value: state.text,
        id: new Date().getTime(),
        checked: false,
        removed: false,
      };
      return { ...state, todos: [newTodo, ...state.todos], text: '' };
    }
    case 'filter':
      return { ...state, filter: action.value };
    case 'edit': {
      const newTodos = state.todos.map((todo) => {
        if (todo.id === action.id) {
          todo.value = action.value;
        }
        return todo;
      });
      return { ...state, todos: newTodos };
    }
    case 'check': {
      const newTodos = state.todos.map((todo) => {
        if (todo.id === action.id) {
          todo.checked = !action.checked;
        }
        return todo;
      });
      return { ...state, todos: newTodos };
    }
    case 'remove': {
      const newTodos = state.todos.map((todo) => {
        if (todo.id === action.id) {
          todo.removed = !action.removed;
        }
        return todo;
      });
      return { ...state, todos: newTodos };
    }
    case 'empty': {
      const newTodos = state.todos.filter((todo) => !todo.removed);
      return { ...state, todos: newTodos };
    }
    default:
      return state;
  }
};

const Selector: React.VFC<{ dispatch: Dispatch<Action> }> = memo(
  ({ dispatch }) => {
    const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => {
      dispatch({ type: 'filter', value: e.target.value as Filter });
    };

    return (
      <select className="select" defaultValue="all" onChange={handleOnFilter}>
        <option value="all">すべてのタスク</option>
        <option value="checked">完了したタスク</option>
        <option value="unchecked">現在のタスク</option>
        <option value="removed">削除済みのタスク</option>
      </select>
    );
  }
);
Selector.displayName = 'Selector';

const EmptyButton: React.VFC<{ dispatch: Dispatch<Action> }> = memo(
  ({ dispatch }) => {
    const handleOnEmpty = () => {
      dispatch({ type: 'empty' });
    };

    return (
      <button className="empty" onClick={handleOnEmpty}>
        ごみ箱を空にする
      </button>
    );
  }
);
EmptyButton.displayName = 'EmptyButton';

const Form: React.VFC<{ state: State; dispatch: Dispatch<Action> }> = memo(
  ({ state, dispatch }) => {
    const handleOnClick = () => {
      dispatch({ type: 'submit' });
    };

    const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      dispatch({ type: 'change', value: e.target.value });
    };

    return (
      <form className="form" onSubmit={handleOnSubmit}>
        <input
          className="text"
          type="text"
          disabled={state.filter === 'checked'}
          value={state.text}
          onChange={handleOnChange}
        />
        <button
          className="button"
          disabled={state.filter === 'checked'}
          value="追加"
          onClick={handleOnClick}
        />
      </form>
    );
  }
);
Form.displayName = 'Form';


const FilteredTodos: React.VFC<{
  state: State;
  dispatch: Dispatch<Action>;
}> = memo(({ state, dispatch }) => {
  const handleOnEdit = (id: number, value: string) => {
    dispatch({ type: 'edit', id, value });
  };

  const handleOnCheck = (id: number, checked: boolean) => {
    dispatch({ type: 'check', id, checked });
  };

  const handleOnRemove = (id: number, removed: boolean) => {
    dispatch({ type: 'remove', id, removed });
  };

  const filteredTodos = state.todos.filter((todo) => {
    switch (state.filter) {
      case 'all':
        return !todo.removed;
      case 'checked':
        return !todo.removed && todo.checked;
      case 'unchecked':
        return !todo.removed && !todo.checked;
      case 'removed':
        return todo.removed;
      default:
        return todo;
    }
  });

  return (
    <div className="align-lists">
      <ul>
        {filteredTodos.map((todo) => {
          return (
            <li key={todo.id}>
              <input
                type="checkbox"
                disabled={todo.removed}
                checked={todo.checked}
                onChange={() => handleOnCheck(todo.id, todo.checked)}
              />
              <input
                className="text"
                type="text"
                disabled={todo.checked || todo.removed}
                value={todo.value}
                onChange={(e) => handleOnEdit(todo.id, e.target.value)}
              />
              <button
                className="button"
                onClick={() => handleOnRemove(todo.id, todo.removed)}>
                {todo.removed ? '復元' : '削除'}
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
});
FilteredTodos.displayName = 'FilteredTodos';

const App: React.VFC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div className="container">
      <Selector dispatch={dispatch} />
      {state.filter === 'removed' ? (
        <EmptyButton dispatch={dispatch} />
      ) : (
        <Form state={state} dispatch={dispatch} />
      )}
      <FilteredTodos state={state} dispatch={dispatch} />
    </div>
  );
};

export default App;

useContextを使った実装

更新方法については useReducer と同じく、アクションをdispatchすることでreducerで処理される。
createContextによって作成された Context.Provider から state と dispatch メソッドを提供することで、propsで渡す必要がなくなる。

const Context = React.createContext('初期値');
const [state, dispatch] = useReducer(reducer, initialState);

<Context.Provider value={{ state, dispatch }}>
  // ここで呼び出されるコンポーネントは、Context.Providerの提供する state と dispatch メソッドを使用することができる。
</Context.Provider>

// これで使用できる
const { state, dispatch } = useContext(Context);
App.tsx
import React, {
  useReducer,
  memo,
  Dispatch,
  createContext,
  useContext
} from 'react';
import './App.css';

interface Todo {
  value: string,
  id: number,
  checked: boolean,
  removed: boolean;
}

type Filter = 'all' | 'checked' | 'unchecked' | 'removed';

interface State {
  text: string;
  todos: Todo[];
  filter: Filter;
}

const initialState: State = {
  text: '',
  todos: [],
  filter: 'all',
};

type Action = 
  { type: 'change', value: string }
  | { type: 'filter', value: Filter }
  | { type: 'submit' }
  | { type: 'empty' }
  | { type: 'edit', id: number, value: string }
  | { type: 'check', id: number, checked: boolean }
  | { type: 'remove', id: number, removed: boolean }

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'change': {
      return { ...state, text: action.value };
    }
    case 'submit': {
      if (!state.text) return state;

      const newTodo: Todo = {
        value: state.text,
        id: new Date().getTime(),
        checked: false,
        removed: false,
      };
      return { ...state, todos: [newTodo, ...state.todos], text: '' };
    }
    case 'filter':
      return { ...state, filter: action.value };
    case 'edit': {
      const newTodos = state.todos.map((todo) => {
        if (todo.id === action.id) {
          todo.value = action.value;
        }
        return todo;
      });
      return { ...state, todos: newTodos };
    }
    case 'check': {
      const newTodos = state.todos.map((todo) => {
        if (todo.id === action.id) {
          todo.checked = !action.checked;
        }
        return todo;
      });
      return { ...state, todos: newTodos };
    }
    case 'remove': {
      const newTodos = state.todos.map((todo) => {
        if (todo.id === action.id) {
          todo.removed = !action.removed;
        }
        return todo;
      });
      return { ...state, todos: newTodos };
    }
    case 'empty': {
      const newTodos = state.todos.filter((todo) => !todo.removed);
      return { ...state, todos: newTodos };
    }
    default:
      return state;
  }
};

const AppContext = createContext(
  {} as {
    state: State;
    dispatch: Dispatch<Action>;
  }
);

const Selector: React.VFC= memo(() => {
  const { state, dispatch } = useContext(AppContext);
  const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => {
    dispatch({ type: 'filter', value: e.target.value as Filter });
  };

  return (
    <select className="select" defaultValue="all" onChange={handleOnFilter}>
      <option value="all">すべてのタスク</option>
      <option value="checked">完了したタスク</option>
      <option value="unchecked">現在のタスク</option>
      <option value="removed">削除済みのタスク</option>
    </select>
  );
});
Selector.displayName = 'Selector';

const EmptyButton: React.VFC = memo(() => {
  const { state, dispatch } = useContext(AppContext);
  const handleOnEmpty = () => {
    dispatch({ type: 'empty' });
  };

  return (
    <button className="empty" onClick={handleOnEmpty}>
      ごみ箱を空にする
    </button>
  );
});
EmptyButton.displayName = 'EmptyButton';

const Form: React.VFC = memo(() => {
  const { state, dispatch } = useContext(AppContext);
  const handleOnClick = () => {
    dispatch({ type: 'submit' });
  };

  const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    dispatch({ type: 'change', value: e.target.value });
  };

  return (
    <form className="form" onSubmit={(e) => e.preventDefault()}>
      <input
        className="text"
        type="text"
        disabled={state.filter === 'checked'}
        value={state.text}
        onChange={handleOnChange}
      />
      <button
        disabled={state.filter === 'checked'}
        onClick={handleOnClick}
      >追加</button>
    </form>
  );
});
Form.displayName = 'Form';


const FilteredTodos: React.VFC = memo(() => {
  const { state, dispatch } = useContext(AppContext);
  const handleOnEdit = (id: number, value: string) => {
    dispatch({ type: 'edit', id, value });
  };

  const handleOnCheck = (id: number, checked: boolean) => {
    dispatch({ type: 'check', id, checked });
  };

  const handleOnRemove = (id: number, removed: boolean) => {
    dispatch({ type: 'remove', id, removed });
  };

  const filteredTodos = state.todos.filter((todo) => {
    switch (state.filter) {
      case 'all':
        return !todo.removed;
      case 'checked':
        return !todo.removed && todo.checked;
      case 'unchecked':
        return !todo.removed && !todo.checked;
      case 'removed':
        return todo.removed;
      default:
        return todo;
    }
  });

  return (
    <div className="align-lists">
      <ul>
        {filteredTodos.map((todo) => {
          return (
            <li key={todo.id}>
              <input
                type="checkbox"
                disabled={todo.removed}
                checked={todo.checked}
                onChange={() => handleOnCheck(todo.id, todo.checked)}
              />
              <input
                className="text"
                type="text"
                disabled={todo.checked || todo.removed}
                value={todo.value}
                onChange={(e) => handleOnEdit(todo.id, e.target.value)}
              />
              <button
                className="button"
                onClick={() => handleOnRemove(todo.id, todo.removed)}>
                {todo.removed ? '復元' : '削除'}
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
});
FilteredTodos.displayName = 'FilteredTodos';

const App: React.VFC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      <div className="container">
        <Selector />
        {state.filter === 'removed' ? (
          <EmptyButton />
        ) : (
          <Form />
        )}
        <FilteredTodos />
      </div>
    </AppContext.Provider>
  );
};

export default App;

Redux(toolkit)を使った実装

1つのStoreを分割して、nameで区切られたSliceを作成。それぞれのSliceごとにStore / Reducer / Actionsが管理される。
下記のtodoSliceでreducersを定義すると、todos/addTodoのActionCreatorが自動生成されるため、Actiontypeでswitch文を書いたりしなくても良い。

// Sliceの例
const todoSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo(state, action) {
      const {id, text} = action.payload;
      state.push({id, text, removed: true})
    }
    ......
  }
})

// configureStore で、Sliceを1つにまとめたstoreを作成
import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {
    todoReducer: todoReducer,
  },
});

// コンポーネントにstoreを渡すことで、どこからでもStoreにアクセスできる
import { store } from './app/store';
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  document.getElementById('root')
);


// useSelectorを使用して呼び出し
import { useSelector, useDispatch } from "react-redux";
const generation = useSelector(todo);

// useDispatchを使用して更新
dispatch(addTodo(todo))

型を定義

src/types/Types.ts
export type Todo = {
  value: string,
  id: number,
  checked: boolean,
  removed: boolean;
};

export type State = {
  text: string;
  todos: Todo[];
  filter: string;
};

Slice

Stateを管理するSliceを作成する。

src/modules/todoModules.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {Todo, State} from '../types/Types';

const initialState: State = {
  text: '',
  todos: [],
  filter: 'all',
};

interface updateParams {
  id: number,
  value: string,
};

const taskModule = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTask(state: State, action: PayloadAction<string>) {
      const newTodo: Todo = {
        value: action.payload,
        id: new Date().getTime(),
        checked: false,
        removed: false,
      };
      return { ...state, todos: [newTodo, ...state.todos], text: ''};
    },
    editTask(state: State, action: PayloadAction<updateParams>) {
      const newTodos = state.todos.map((todo: Todo) => {
        if(todo.id === action.payload.id) {
          todo.value = action.payload.value;
        };
        return todo;
      });
      return { ...state, todos: newTodos }
    },
    checkTask(state: State, action: PayloadAction<Todo>) {
      state.todos.map((todo: Todo) => {
        if (todo.id === action.payload.id) {
          todo.checked = !action.payload.checked;
        }
        return todo;
      });
    },
    removeTask(state: State, action: PayloadAction<Todo>) {
      state.todos.map((todo) => {
        if (todo.id === action.payload.id) {
          todo.removed = !action.payload.removed;
        }
        return todo;
      });
    },
    emptyTask(state: State) {
      const newTodos: Todo[] = state.todos.filter((todo) => !todo.removed);
      return  { ...state, todos: newTodos };
    },
    changeFilter(state: State, action: PayloadAction<string>) {
      state.filter = action.payload
      return state;
    },
    changeText(state: State, action: PayloadAction<string>) {
      state.text = action.payload;
      return state;
    },
  }
});

export const {
  addTask, editTask, checkTask, removeTask, emptyTask, changeFilter, changeText,
} = taskModule.actions;

export default taskModule;

ストア

作成したSliceを一つにまとめてstoreにする。

src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';

const store = configureStore({
  reducer: rootReducer,
});

export type AppDispatch = typeof store.dispatch;
export default store;

ルート

storeをAppコンポーネント以下に適用する

src/index.tsx
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

メイン

src/App.tsx
import React from 'react';
import { useSelector } from 'react-redux';
import { RootState } from './rootReducer';
import './App.css';

import Selector from './components/Selector';
import Form from './components/Form';
import EmptyButton from './components/EmptyButton';
import FilteredTodos from './components/FilteredTodos';

const App: React.VFC = () => {
  const { filter } = useSelector((state: RootState) => state.filter);

  return (
    <div className="container">
      <Selector />
      {filter === 'removed' ? (
        <EmptyButton />
      ) : (
        <Form />
      )}
      <FilteredTodos />
    </div>
  );
};

export default App;

コンポーネント

各コンポーネントでは、useSelectorを用いてstoreの値を取得し、useDispatchを用いてstoreの値を更新する。

src/components/Form.tsx
import React, {useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {addTask} from '../modules/taskModules';
import { RootState } from '../rootReducer';

function Form() {
  const dispatch = useDispatch();
  const { filter } = useSelector((state: RootState) => state.filter);
  const [inputText, setInputText] = useState<string>('');
  const handleSubmit = () => {
    dispatch(addTask(inputText));
    setInputText('');
  }

  return (
    <div className="form">
      <input
        className="text"
        type="text"
        disabled={filter === 'checked'}
        value={inputText}
        onChange={(e) => setInputText(e.target.value)}
      />
      <button
        className="button"
        disabled={filter === 'checked'}
        onClick={handleSubmit}
      >追加</button>
    </div>
  );
};

export default Form;
src/components/Selector.tsx
import React from 'react';
import {useDispatch} from 'react-redux';
import {changeFilter} from '../modules/taskModules';

function Selector() {
  const dispatch = useDispatch();

  return (
    <select className="select" defaultValue="all" onChange={(e) => dispatch(changeFilter(e.target.value))}>
      <option value='all'>すべてのタスク</option>
      <option value='checked'>完了したタスク</option>
      <option value='unchecked'>現在のタスク</option>
      <option value='removed'>削除済みのタスク</option>
    </select>
  );
};

export default Selector;
src/components/FilteredTodos.tsx
import React from 'react';
import TodoItem from './TodoItem';
import {useSelector} from 'react-redux';
import { RootState } from '../rootReducer';

function FilteredTodos() {
  const { todos } = useSelector((state: RootState) => state.todos);
  const { filter } = useSelector((state: RootState) => state.filter);

  const filteredTodos = todos.filter((todo: any) => {
    switch (filter) {
      case 'all':
        return !todo.removed;
      case 'checked':
        return !todo.removed && todo.checked;
      case 'unchecked':
        return !todo.removed && !todo.checked;
      case 'removed':
        return todo.removed;
      default:
        return todo;
    }
  });

  return (
    <div className="align-lists">
      <ul>
        {filteredTodos.map((todo: any) => {
          return (
            <TodoItem todo={todo} />
          );
        })}
      </ul>
    </div>
  );
};

export default FilteredTodos;
src/components/TodoItem.tsx
import React, {useState} from 'react';
import { useDispatch } from 'react-redux';
import { editTask, checkTask, removeTask } from '../modules/taskModules';
import {Todo} from '../types/Types';

type Props = {
  todo: Todo
}
function TodoItem(props: Props) {
  const todo: Todo = props.todo;
  const dispatch = useDispatch();
  const [inputText, setInputText] = useState(todo.value);

  const handleUpdateTodo = (e: any) => {
    setInputText(e.target.value);
    const params = {
      id: todo.id,
      value: inputText,
    }
    dispatch(editTask(params));
  };
  return (
    <li key={todo.id}>
      <input
        type="checkbox"
        disabled={todo.removed}
        checked={todo.checked}
        onChange={() => dispatch(checkTask(todo))}
      />
      <input
        className="text"
        type="text"
        disabled={todo.checked || todo.removed}
        value={inputText}
        onChange={handleUpdateTodo}
      />
      <button
        className="button"
        onClick={() => dispatch(removeTask(todo))}>
        {todo.removed ? '復元' : '削除'}
      </button>
    </li>
  );
}

export default TodoItem;
src/components/EmptyButton.tsx
import React from 'react';
import {useDispatch} from 'react-redux';
import {emptyTask} from '../modules/taskModules';

function EmptyButton() {
  const dispatch = useDispatch();
  return (
    <button className="empty" onClick={() => dispatch(emptyTask())}>
      ごみ箱を空にする
    </button>
  );
};

export default EmptyButton;

Recoilを使った実装

stateは atom というストアで管理される。
RecoilRootで囲ったコンポーネント群は atom を共有でき、値を取得・更新することができる。

import { RecoilRoot } from "recoil";

<RecoilRoot>
  <App />
</RecoilRoot>

Todoの型を定義

types/Todo.tsx
type Todo = {
  value: string,
  id: number,
  checked: boolean,
  removed: boolean,
}
export default Todo;

メイン

App.tsx
import React from 'react';
import './App.css';
import Selector from './components/Selector';
import Form from './components/Form';
import FilteredTodos from './components/TodoList';
import EmptyButton from './components/EmptyButton';
import { searchTextFormState } from './atoms/SearchToDoFormAtom';
import { useRecoilValue } from "recoil";

const App: React.FC = () => {
  const filter: string = useRecoilValue(searchTextFormState);
  return (
    <div className="container">
      <Selector />
      {filter === 'removed' ? (
        <EmptyButton />
      ) : (
        <Form />
      )}
      <FilteredTodos />
    </div>
  );
}

export default App;

コンポーネント

各コンポーネントで atom を利用して画面を動的に変更する。
atom に格納されている state は、useRecoilValueメソッドによって取得できる。
atom に格納されている state を更新したい場合は、useSetRecoilStateメソッドによってセッターを定義し、セッターに更新したい値を渡す。

import atom from 'atom';
import { useRecoilValue, useSetRecoilState } from 'recoil';

const atom = useRecoilValue(atom);
const setAtom = useSetRecoilState(atom);

setAtom('更新したい値')
components/Form.tsx
import React, { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from 'recoil';
import { todoTitleFormState } from '../atoms/AddToDoFormAtom';
import { todoListState } from '../atoms/ToDoListAtom';
import { searchTextFormState } from '../atoms/SearchToDoFormAtom';
import Todo from "../types/Todo";

const Form: React.FC = () => {
  const todoList: Todo[] = useRecoilValue(todoListState);
  const todoTitleFormValue: string = useRecoilValue(todoTitleFormState);
  const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
  const setTodoTitleFormValue: SetterOrUpdater<string> = useSetRecoilState(
    todoTitleFormState
  );
  const filter: string = useRecoilValue(searchTextFormState);

  const onChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setTodoTitleFormValue(event.target.value);
    },
    [setTodoTitleFormValue]
  );
  const onClick = useCallback(() => {
    const newTodo = {
      value: todoTitleFormValue,
      id: new Date().getTime(),
      checked: false,
      removed: false,
    };
    setTodoList([...todoList, newTodo]);
    setTodoTitleFormValue('');
  }, [todoList, todoTitleFormValue, setTodoList, setTodoTitleFormValue]);

  return (
    <form className="form" onSubmit={(e) => e.preventDefault()}>
      <input
        className="text"
        type="text"
        disabled={filter === 'checked'}
        value={todoTitleFormValue}
        onChange={onChange}
      />
      <button
        disabled={filter === 'checked'}
        onClick={onClick}
      >追加</button>
    </form>
  )
}

export default Form;
components/Selector.tsx
import React, { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from 'recoil';
import { searchTextFormState } from '../atoms/SearchToDoFormAtom'; 

const Selector: React.FC = () => {
  const searchTextFormValue: string = useRecoilValue(searchTextFormState);
  const setSearchTextFormValue: SetterOrUpdater<string> = useSetRecoilState(
    searchTextFormState
  );
  const onChange = useCallback(
    (event: React.ChangeEvent<HTMLSelectElement>) => {
      setSearchTextFormValue(event.target.value);
    },
    [setSearchTextFormValue]
  );

  return (
    <select className="select" value={searchTextFormValue} defaultValue="all" onChange={onChange}>
      <option value="all">すべてのタスク</option>
      <option value="checked">完了したタスク</option>
      <option value="unchecked">現在のタスク</option>
      <option value="removed">削除済みのタスク</option>
    </select>
  )
}
export default Selector;
components/FilteredTodos.tsx
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from 'recoil';
import { searchedTodoListSelector } from '../selectors/SearchedTodoListSelector';
import { todoListState } from '../atoms/ToDoListAtom';
import Todo from '../types/Todo';

const FilteredTodos: React.FC = () => {
  const list: Todo[] = useRecoilValue(searchedTodoListSelector);
  const todoList: Todo[] = useRecoilValue(todoListState);
  const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
  const onChange = useCallback((id: number, checked: boolean) => {
    const newTodoList = todoList.map((todo) => {
      const newTodo = {...todo};
      if(todo.id === id) {
        newTodo.checked = !checked
      }
      return newTodo;
    })
    setTodoList(newTodoList);
  }, [todoList, setTodoList]);
  const onClick = useCallback((id: number, removed: boolean) => {
    const newTodoList = todoList.map((todo) => {
      const newTodo = {...todo}
      if(todo.id === id) {
        newTodo.removed = !removed
      }
      return newTodo;
    })
    setTodoList(newTodoList);
  }, [todoList, setTodoList]);
  return (
    <div className="align-lists">
      <ul>
        {
          list.map((todo: Todo) => {
            return (
              <li key={todo.id}>
                <input
                  type="checkbox"
                  disabled={todo.removed}
                  checked={todo.checked}
                  onChange={() => onChange(todo.id, todo.checked)}
                />
                <input
                  className="text"
                  type="text"
                  disabled={todo.checked || todo.removed}
                  value={todo.value}
                />
                <button
                  className="button"
                  onClick={() => onClick(todo.id, todo.removed)}
                >
                  {todo.removed ? '復元' : '削除'}
                </button>
              </li>
            );
          })
        }
      </ul>
    </div>
  )
}

export default FilteredTodos;
components/EmptyButton.tsx
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { todoListState } from "../atoms/ToDoListAtom";
import Todo from "../types/Todo";

const EmptyButton: React.FC = () => {
  const todoList: Todo[] = useRecoilValue(todoListState);
  const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);

  const onClick = useCallback(() => {
    const newTodos = todoList.filter((todo) => !todo.removed);
    setTodoList(newTodos);
  }, [setTodoList]);
  return (
    <button className="empty" onClick={onClick}>
      ごみ箱を空にする
    </button>
  )
}

export default EmptyButton;

atom

ここでstateを管理する
atom は key とデフォルトの値を持つ

atoms/AddTodoFormAtom.tsx
import { atom } from "recoil";

export const todoTitleFormState = atom<string>({
    key: "todoTitleForm",
    default: '',
});
atoms/SearchTodoFormAtom.tsx
import { atom } from "recoil";

export const searchTextFormState = atom<string>({
    key: "searchTextForm",
    default: '',
});
atoms/TodoListAtom.tsx
import { atom } from "recoil";
import Todo from '../types/Todo';

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

selector

atomの値を加工して返す時に使う。
ここでは、selector の値によって、タスクのリストをフィルタして返している。
atom と同じく、useRecoilValue を利用して値を取得できる。

import selector from 'selector';
import { useRecoilValue } from 'recoil';

const list = useRecoilValue(selector);

ここでは、selectorの値をatomから取得して、これを元にタスクのリストをフィルタして返している。

selectors/SearchedTodoListSelector.tsx
import { selector } from 'recoil';
import { todoListState } from '../atoms/ToDoListAtom';
import { searchTextFormState } from '../atoms/SearchToDoFormAtom';
import Todo from '../types/Todo';

export const searchedTodoListSelector = selector<Todo[]>({
  key: "searchedTodoListSelector",
  get: ({ get }) => {
    const todoList: Todo[] = get(todoListState);
    const filter: string = get(searchTextFormState);
    return todoList.filter((todo) => {
      switch (filter) {
        case 'all':
          return !todo.removed;
        case 'checked':
          return !todo.removed && todo.checked;
        case 'unchecked':
          return !todo.removed && !todo.checked;
        case 'removed':
          return todo.removed;
        default:
          return todo;
      }
    })
  },
});

それぞれの使い所

useState vs useReducer

本質的には同等。コンポーネントにステートを持たせるのが役割。
useReducerは、複数の関連するステート(オブジェクト)を扱うときが使い所。
useStateではステートを更新する際に更新するための関数を呼び出すのに対して、useReducerでは定義したreducerにメッセージを送るだけなのでシンプルになりやすい。

ユーザーデータをAPIから取得してきて一覧表示する例
export function App() {
  const [{loading, users}, setState] = useState({loading: false, users: []})
  return (
    {loading? (
      <p>ロード中</p>
    ):(
      <ul>
        {users.map((user) => {
          <li key={user.id}>{user.name}</li>>}
        )}
      </ul>
    )}
    <button onClick={async() => {
      setState((state) => ({...state, loading: true}))
      const {users} = await fakeApi()
      setState((state) => ({...state, loading: false, users})
    }}>
      データを取得
    </button>
  )
}

export function App() {
  const reducer = (state, action) => {
    switch(action.type) {
      case '':
        return {...state, loading: true}
      case '':
        const {users} = action.payload;
        return {...state, loading: false, users}
      default:
        return state;
    };
  };
  const [{loading, users}, dispatch] = useReducer(reducer, {loading: false, users: []}
  return (
    <div>
      {loading? (
        <p>ロード中</p>
      ):(
        <ul>
          {users.map((user) => {
            <li key={user.id}>{user.name}</li>}
          )}
        </ul>
      )}
      <button onClick={async() => {
        dispatch({type: 'pending'})
        const {users} = await fakeAPI()
        dispatch({type: 'done', payload: {users}})
      }}>
        データを取得
      </button>
    </div>
  )
}
16
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
16
14