BETA

ReduxのサンプルTODOアプリをRecoilを使って書く

投稿日:2020-05-18
最終更新:2020-05-18

概要

Recoilがアツいですね。
そこで今回はReduxのサンプルTODOアプリをRecoilを使って書いてみました。
前に書いた記事(React + TypeScriptでTODOアプリ (with React Hooks))のコードを流用しています。

本編

早速ですがコード全体です。

// based on https://redux.js.org/basics/example/  

import * as React from 'react'  
import * as ReactDOM from 'react-dom'  
import shortid from 'shortid'  
import {  
  RecoilRoot,  
  atom,  
  selector,  
  useSetRecoilState,  
  useRecoilState,  
  useRecoilValue,  
} from 'recoil'  

enum VisibilityFilters {  
  SHOW_ALL,  
  SHOW_COMPLETED,  
  SHOW_ACTIVE,  
}  

interface Todo {  
  id: string  
  text: string  
  completed: boolean  
}  

const todosState = atom({  
  key: 'todosState',  
  default: [] as Todo[],  
})  

const filterState = atom({  
  key: 'filterState',  
  default: VisibilityFilters.SHOW_ALL,  
})  

const useAddTodo = () => {  
  const setTodo = useSetRecoilState(todosState)  
  const addTodo = React.useCallback(  
    (text: string) => {  
      setTodo((todos: Todo[]) => [  
        ...todos,  
        { id: shortid.generate(), text, completed: false },  
      ])  
    },  
    [setTodo],  
  )  

  return addTodo  
}  

const AddTodo = () => {  
  const addTodo = useAddTodo()  
  const [input, setInput] = React.useState('')  

  return (  
    <form  
      onSubmit={(e) => {  
        e.preventDefault()  
        if (!input.trim()) {  
          return  
        }  
        addTodo(input)  
        setInput('')  
      }}  
    >  
      <input  
        type="text"  
        name="todo"  
        value={input}  
        onChange={(e) => {  
          setInput(e.target.value || '')  
        }}  
      />  
      <button type="submit">Add Todo</button>  
    </form>  
  )  
}  

const getVisibleTodos = (todos: Todo[], filter: VisibilityFilters) => {  
  switch (filter) {  
    case VisibilityFilters.SHOW_ALL:  
      return todos  
    case VisibilityFilters.SHOW_COMPLETED:  
      return todos.filter((t) => t.completed)  
    case VisibilityFilters.SHOW_ACTIVE:  
      return todos.filter((t) => !t.completed)  
    default:  
      throw new Error('Unknown filter: ' + filter)  
  }  
}  

const visibleTodosState = selector({  
  key: 'visibleTodosState',  
  get: ({ get }) => getVisibleTodos(get(todosState), get(filterState)),  
})  

const useChangeVisibility = () => {  
  const setTodos = useSetRecoilState(todosState)  
  const changeVisibility = React.useCallback(  
    (todoId: string) => {  
      setTodos((preTodos: Todo[]) =>  
        preTodos.map((preTodo) =>  
          preTodo.id === todoId  
            ? {  
                ...preTodo,  
                completed: !preTodo.completed,  
              }  
            : preTodo,  
        ),  
      )  
    },  
    [setTodos],  
  )  

  return changeVisibility  
}  

const VisibleTodoList = () => {  
  const visibleTodos = useRecoilValue(visibleTodosState)  
  const changeVisibility = useChangeVisibility()  

  return (  
    <ul>  
      {visibleTodos.map((todo) => (  
        <li  
          key={todo.id}  
          onClick={() => changeVisibility(todo.id)}  
          style={{  
            textDecoration: todo.completed ? 'line-through' : 'none',  
          }}  
        >  
          {todo.text}  
        </li>  
      ))}  
    </ul>  
  )  
}  

interface FilterLinkProps {  
  filter: VisibilityFilters  
}  

const FilterLink: React.FC<FilterLinkProps> = ({ filter, children }) => {  
  const [filterStateValue, setFilter] = useRecoilState(filterState)  

  return (  
    <button  
      onClick={() => setFilter(filter)}  
      disabled={filter === filterStateValue}  
      style={{  
        marginLeft: '4px',  
      }}  
    >  
      {children}  
    </button>  
  )  
}  

const Footer = () => (  
  <>  
    <span>Show: </span>  
    <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>  
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>  
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>  
  </>  
)  

const App = () => (  
  <RecoilRoot>  
    <AddTodo />  
    <VisibleTodoList />  
    <Footer />  
  </RecoilRoot>  
)  

ReactDOM.render(<App />, document.getElementById('root'))  

あとはこれをhtmlで読み込んでparcelで動かしてます。

私の前の記事のコードと比較しながら解説しますが、本当は(hooksを使った)Reduxを使ったコードと比較したほうが良いと思います。
まず、前の記事では

interface State {  
  todos: Todo[]  
  filter: VisibilityFilters  
}  

const initialState: State = {  
  todos: [],  
  filter: VisibilityFilters.SHOW_ALL,  
}  

がアプリ全体の状態でしたが、今回は

const todosState = atom({  
  key: 'todosState',  
  default: [] as Todo[],  
})  

const filterState = atom({  
  key: 'filterState',  
  default: VisibilityFilters.SHOW_ALL,  
})  

という2つのatomがそれに当たります。keyはユニークなら良いです。
前の記事でReact.useContextstatedispatchを持ってきた部分(Reduxであれば、useDispatchuseSelectorを使うと思います)は、ユースケースによって何を使うかが変わります。
一番簡単なのはuseRecoilStateで、これはReact.useStateの、引数がatom(もしくはselector)版です。

const FilterLink: React.FC<FilterLinkProps> = ({ filter, children }) => {  
  const [filterStateValue, setFilter] = useRecoilState(filterState)  

  return (  
    <button  
      onClick={() => setFilter(filter)}  
      disabled={filter === filterStateValue}  
      style={{  
        marginLeft: '4px',  
      }}  
    >  
      {children}  
    </button>  
  )  
}  

これの亜種としてuseRecoilValueuseSetRecoilStateがあります。それぞれuseRecoilStateの返り値の左側だけ欲しい場合(つまり値の読み取りだけしたい場合)、右側だけ欲しい場合(つまり値の更新だけしたい場合)に使います。

さて、atomとは別にselectorというのがいます。これはatomと同様に使えますが、atomをsubscribeして、getに沿って値を返します。これのよいところは、subscribeしたatomの値が変わってもこいつの値が変わらなければ再レンダリングが抑制されることです。下のケースだとCompletedなタスクを表示しているときに新しいタスクを追加したとき、にあたります。

const visibleTodosState = selector({  
  key: 'visibleTodosState',  
  get: ({ get }) => getVisibleTodos(get(todosState), get(filterState)),  
})  

// 使うときはatomと同様に使える  
const VisibleTodoList = () => {  
  const visibleTodos = useRecoilValue(visibleTodosState)  
  const changeVisibility = useChangeVisibility()  

  return (  
    <ul>  
      {visibleTodos.map((todo) => (  
        <li  
          key={todo.id}  
          onClick={() => changeVisibility(todo.id)}  
          style={{  
            textDecoration: todo.completed ? 'line-through' : 'none',  
          }}  
        >  
          {todo.text}  
        </li>  
      ))}  
    </ul>  
  )  
}  

今回は使いませんでしたがsetを指定することで書き込み可能なselectorも作れます。

そして最後にRecoilRootでアプリを囲います。前の記事だとAppContext.Provider、ReduxだとProviderに当たる部分です。

const App = () => (  
  <RecoilRoot>  
    <AddTodo />  
    <VisibleTodoList />  
    <Footer />  
  </RecoilRoot>  
)  

感想

書いてみた感想ですが、Reduxのstateが細かく別れたatomになり、reducerがcustom hookや(今回は使わなかったのですが)selectorsetに対応するんじゃないでしょうか。
非同期処理も対応しているので、そっちも試してみたいです。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@Catminusminusの技術ブログ Frontend/Machine Learning/C++など、興味のあるものについて、小ネタや調査中のこと、備忘録を書いたりします

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう