Обложка статьи «Пишем приложение со списком дел при помощи React Hooks»

Пишем приложение со списком дел при помощи React Hooks

React Hooks — это функции, которые позволяют определять категорию состояния и жизненный цикл (state, lifecycle) React-компонента без использования ES6-классов.

Некоторые преимущества React Hooks:

  • изолированная логика упрощает последующие тесты;
  • при распределении логики не понадобится использование render props или «компонентов высшего порядка»;
  • разделение функциональности приложения основано на логике, а не на жизненном цикле;
  • React Hooks — достойная замена ES6-классов, с которыми порой возникают проблемы даже у опытных программистов.

Для демонстрации возможностей React Hooks построим приложение, в котором можно добавлять и удалять задачи (ToDo-приложение).

Так будет выглядеть готовое приложение

Это приложение будет выполнять следующие функции:

  • отображать ваши текущие задачи;
  • позволять добавлять новые задачи через поле ввода;
  • удалять задачи.

Установка

Весь код доступен на GitHub и CodeSandbox.

git clone https://github.com/yazeedb/react-hooks-todo
cd react-hooks-todo
npm install

Ветка master — уже готовый проект. Если вы хотите создавать приложение поэтапно, следуя статье, используйте ветку start.

git checkout start

Для запуска проекта используйте следующую команду:

npm start

Приложение должно запуститься на localhost:3000. Вы увидите пустой начальный интерфейс с названием приложения. Весь дизайн уже настроен при помощи библиотеки material-ui, так что можно сразу добавлять функциональность.

Todo-форма

Создайте новый файл src/TodoForm.js и добавьте в него следующий код:

import React from 'react';
import TextField from '@material-ui/core/TextField';
const TodoForm = ({ saveTodo }) => {
  return (
    <form>
      <TextField
        variant="outlined"
        placeholder="Add todo"
        margin="normal"
      />
   </form>
  );
};


export default TodoForm;

Как можно понять из названия формы, здесь главная задача — добавить что-либо в состояние React-компонента. Вот и первый hook.

useState

Теперь давайте рассмотрим такой код:

import { useState } from 'react';
const [value, setValue] = useState('');

Это функция, которая принимает начальное состояние React-компонента и возвращает массив. Вы можете использовать console.log, чтобы посмотреть, что именно она возвращает.

Под первым индексом массива находится текущее значение состояния компонента, а во второй ячейке находится обновляющая функция. Они названы value и setValue, следуя ES6 destructuring assignment.

useState с формами

Ваша форма должна отслеживать значение, вводимое пользователем и вызывать метод saveTodo() при отправке формы. useState поможет вам с этим.

Добавьте изменения в TodoForm.js, они выделены жирным шрифтом:

import React, { useState } from 'react';
import TextField from '@material-ui/core/TextField';
const TodoForm = ({ saveTodo }) => {
    const [value, setValue] = useState('');
    return (
    <form 
onSubmit={event => {
        event.preventDefault();
        saveTodo(value);
      }}
    >
      <TextField
        variant="outlined"
        placeholder="Add todo"
        margin="normal" 
        onChange={event => {
          setValue(event.target.value);
        }}
        value={value}
        />
    </form>
  );
};
export default TodoForm;

В index.js импортируйте форму:

...
import TodoForm from './TodoForm';
...
const App = () => {
  return (
    <div className="App">
    <Typography component="h1" variant="h2">
      Todos
    </Typography> <TodoForm saveTodo={console.warn} /> </div>
  );
};

Теперь введённое значение логируется при подтверждении (нажатии на Enter).

useState с Todos

Вам также нужно состояние компонента. Импортируйте useState в index.js. Начальное состояние компонента должно быть пустым массивом.

import React, { useState } from 'react';
...
const App = () => {
  const [todos, setTodos] = useState([]);
  return ...

Компонент TodoList

Для начала создайте новый файл src/TodoList.js.

Большая часть кода — это компоненты из Material-UI. Самые важные части выделены жирным шрифтом.

import React from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import ListItemText from '@material-ui/core/ListItemText';
import Checkbox from '@material-ui/core/Checkbox';
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from '@material-ui/icons/Delete';

const TodoList = ({ todos, deleteTodo }) => (
  <List>
    {todos.map((todo, index) => (
      <ListItem key={index.toString()} dense button>
        <Checkbox tabIndex={-1} disableRipple />
        <ListItemText primary={todo} />
        <ListItemSecondaryAction>
<IconButton
            aria-label="Delete"
            onClick={() => {
              deleteTodo(index);
            }}
          >
            <DeleteIcon />
          </IconButton>
        </ListItemSecondaryAction>
      </ListItem>
    ))}
  </List>
);

export default TodoList;

Чтобы всё работало корректно, требуется:

  • todos — массив всех ваших задач. При помощи метода map() вы сопоставляете каждую из них и создаёте элемент списка;
  • deleteTodo() — функция вызывается при нажатии на IconButton и передает индекс, который однозначно идентифицирует пункт списка.

Импортируйте этот компонент в ваш файл index.js.

...
import TodoList from './TodoList';
import './styles.css';
const App = () => { ...

И используйте в App-функции:

...
<TodoForm saveTodo={console.warn} />
<TodoList todos={todos} />

Добавление новых задач

В index.js добавляем метод SaveTodo()в todo-форму.

<TodoForm
  saveTodo={todoText => {
    const trimmedText = todoText.trim();
    if (trimmedText.length > 0) {
      setTodos([...todos, trimmedText]);
    }
  }}
/>

Проще всего объединить уже существующие задачи с новой. Дополнительные пробелы будут вырезаны.

Очистка поля ввода

Заметьте, что на данном этапе после добавления новой задачи поле ввода не очищено, и это не очень-то хорошо. Исправить это можно внеся маленькое изменение в TodoForm.js.

<form
  onSubmit={event => {
    event.preventDefault();
    saveTodo(value);
   setValue('');
   }}
>

Теперь, как только todo будет сохранён, состояние формы преобразуется в пустую строку:

Удаление задач

Так как TodoList содержит индексы всех задач, можно без проблем найти нужный элемент для удаления.

// TodoList.js
<IconButton
  aria-label="Delete"
  onClick={() => {
    deleteTodo(index);
  }}
>
  <DeleteIcon />
</IconButton>

Теперь примените в index.js.

<TodoList
  todos={todos}
deleteTodo={todoIndex => {
    const newTodos = todos
      .filter((_, index) => index !== todoIndex); 
    setTodos(newTodos);
  }}


/>

Если задача с соответствующим index не найдена, остальные задачи останутся в состоянии формы благодаря использованию setTodos().

Извлечение useState из компонента Todos

В начале статьи упоминалось, что React Hooks хорошо подходят для разделения состояния и логики компонента. И вот как это могло бы выглядеть в вашем приложении.

Создайте новый файл src/useTodoState.js.

import { useState } from 'react';
export default initialValue => {
  const [todos, setTodos] = useState(initialValue);

  return {
    todos,
    addTodo: todoText => {
      setTodos([...todos, todoText]);
    },
    deleteTodo: todoIndex => {
      const newTodos = todos
        .filter((_, index) => index !== todoIndex);

      setTodos(newTodos);
    }
  };
};

По сути это тот же самый код, что и в index.js, но теперь управление состоянием не так плотно связано с компонентом.

Осталось только всё импортировать. Новый код выделен жирным шрифтом.

import React from 'react';
import ReactDOM from 'react-dom';
import Typography from '@material-ui/core/Typography';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
import useTodoState from './useTodoState';
import './styles.css';
const App = () => {
  const { todos, addTodo, deleteTodo } = useTodoState([]);

  return (
    <div className="App">
      <Typography component="h1" variant="h2">
        Todos
      </Typography>

      <TodoForm
        saveTodo={todoText => {
          const trimmedText = todoText.trim();

          if (trimmedText.length > 0) {
            addTodo(trimmedText);
          }
        }}
      />

      <TodoList todos={todos} deleteTodo={deleteTodo} />
    </div>
  );
};

const rootElement = document.getElementById('root');
ReactDOM.render(, rootElement);

Абстракция useState в форме ввода

То же самое можно сделать с формой.

Создайте новый файл src/useInputState.js.

import { useState } from 'react';
export default initialValue => {
  const [value, setValue] = useState(initialValue);

  return {
    value,
    onChange: event => {
      setValue(event.target.value);
    },
    reset: () => setValue('')
  };
};

Сейчас TodoForm.js должен выглядеть так:

import React from 'react';
import TextField from '@material-ui/core/TextField';
import useInputState from './useInputState';
const TodoForm = ({ saveTodo }) => {
  const { value, reset, onChange } = useInputState('');

  return (
    <form
      onSubmit={event => {
        event.preventDefault();

        saveTodo(value);
        reset();
      }}
    >
      <TextField
        variant="outlined"
        placeholder="Add todo"
        margin="normal"
        onChange={onChange}
        value={value}
      />
    </form>
  );
};

export default TodoForm;

В данной статье мы рассмотрели создание ToDo-приложения с помощью React Hooks. Если вы любите React и хотите узнать о нём больше, обратите внимание на нашу статью:

 

Перевод статьи «How to Build a Todo List with React Hooks»