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»