Hello React Hooks

React からだいぶ離れている間に、16.8 で Hooks が出て、React を使った Web アプリ開発パラダイムがかなり変わったようだ。Hooks 以前と以後に分かれるのでは、と思うくらいに。久しぶりに仕事で React を触ることになりそうなので、Hooks は試しておかなければ。簡単な Todo リストのサンプルを書いてみることにした。

プロジェクトは create-react-app で新規作成。create-react-app は TypeScript をサポートしているので、ちゃんとした Web アプリを React で開発するなら、もはや言語は TypeScript 一択だろう。

npx create-react-app react-todo --template typescript

Hooks は useState だけを使ってみた。App.tsx は次の通り。Todo リストを保持するグローバルなステートを App に持たせ、TodoForm にローカルなステートとして編集内容を持たせている。

import React, { useState } from 'react';

interface Todo {
  id?: number;
  name: string;
  completed: boolean;
  editing: boolean;
}

interface TodoFormProps {
  todo: Todo;
  onSave: (todo: Todo) => void;
}

const TodoForm: React.FC<TodoFormProps> = ({
  todo,
  onSave
}) => {
  const [name, setName] = useState(todo.name);
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      onSave({
        ...todo,
        name,
        editing: false,
      });
      setName("");
    }}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}/>
      <button type="submit">
        Save
      </button>
      {
        todo.id &&
        <button onClick={(e) => {
          e.preventDefault();
          onSave({
            ...todo,
            editing: false,
          });
        }}>
          Cancel
        </button>
      }
    </form>
  );
};

interface TodoItemProps {
  todo: Todo;
  onEdit: (todo: Todo) => void;
  onDelete: (todo: Todo) => void;
  onComplete: (todo: Todo) => void;
}

const TodoItem: React.FC<TodoItemProps> = ({
  todo,
  onComplete,
  onEdit,
  onDelete
}) => (
  <div>
    <input
      type="checkbox"
      checked={todo.completed}
      onChange={() => onComplete(todo)}
      />
    {
      todo.completed ?
      <del>{todo.name}</del> :
      <>{todo.name}</>
    }
    <button onClick={() => onEdit(todo)}>
      Edit
    </button>
    <button onClick={() => onDelete(todo)}>
      Del
    </button>
  </div>
);

interface TodoListProps {
  todoList: Todo[];
  onEdit: (todo: Todo) => void;
  onDelete: (todo: Todo) => void;
  onUpdate: (todo: Todo) => void;
  onComplete: (todo: Todo) => void;
}

const TodoList: React.FC<TodoListProps> = ({
  todoList,
  onEdit,
  onComplete,
  onUpdate,
  onDelete,
}) => (
  <div>
    {todoList.map(todo => (
      todo.editing ?
        <TodoForm
          todo={todo}
          onSave={onUpdate}/> :
        <TodoItem
          todo={todo}
          onEdit={onEdit}
          onComplete={onComplete}
          onDelete={onDelete}
        /> 
    ))}
  </div>
);

const App: React.FC = () => {
  const [todoList, setTodoList] = useState<Todo[]>([
    { id: 1, name: "foo", completed: false, editing: false },
    { id: 2, name: "bar", completed: false, editing: false },
  ]);
  const [nextId, setNextId] = useState<number>(3);
  return (
    <div className="App">
      <h1>React Todo</h1>
      <TodoForm
        todo={{name:"", completed: false, editing: false}}
        onSave={(todo) => {
          setTodoList([...todoList, { ...todo, id: nextId }]);
          setNextId(nextId + 1);
        }}
        />
      <TodoList 
        todoList={todoList}
        onDelete={(todo) => {
          setTodoList(todoList.filter(x => x.id !== todo.id));
        }}
        onEdit={(todo) => {
          setTodoList(todoList.map(x => {
            if (x.id === todo.id) {
              return {
                ...x,
                editing: true,
              };
            } else {
              return {
                ...x,
                editing: false,
              };
            }
          }));
        }}
        onUpdate={(todo) => {
          setTodoList(todoList.map(x => {
            if (x.id === todo.id) {
              return todo;
            } else {
              return {
                ...x,
                editing: false,
              };
            }
          }));
        }}
        onComplete={(todo) => {
          setTodoList(todoList.map(x => {
            if (x === todo) {
              return {
                ...x,
                completed: !x.completed
              };
            } else {
              return x;
            }
          }));
        }}
        />
    </div>
  );
}

export default App;

yarn start で実行して動作確認。その場編集モードもうまく動いてくれた。 余談だけど、スクリーンを録画したアニメーション GIF を作るのに ScreenToGif というツールを使ってみたら、かなり簡単に作れてすこぶる良かった。アニメーション GIF があると分かりやすさが段違いなので、積極的に活用していきたい。

f:id:griefworker:20191211120254g:plain

今回は useState だけ試したけど、他にも useEffect や useContext や useReducer など、色んなフックがある。さらにはカスタムフックまで。「React Hooks 完全に理解した」って言えるまで、まだまだ遠いな。