Введение

Основы React

React - JavaScript библиотека для отображения интерфейса пользователя (UI).

React vs JavaScript

JavaScript: императивный подход — достижение результата с помощью выполнения последовательности действий.

React: декларативный подход — подробное описание конечного результата.

Компоненты

Приложения на React состоят из компонентов. Компонент - это часть пользовательского интерфейса, которая имеет свою логику и внешний вид. Он может быть маленьким, как кнопка, или большим, как целая страница.

Если часть UI используется несколько раз или достаточно сложна сама по себе, ее делают отдельным компонентом.

Компонент React представляет собой JavaScript функцию, возвращающую разметку. Имя компонента всегда начинается с заглавной буквы, в то время как HTML-тег должен начинаться с маленькой.

Написание JSX разметки

JSX представляет из себя объекты. Babel компилирует JSX в вызовы React.createElement().

const element = <h1 className="greeting">Привет, мир!</h1>; const element = React.createElement( 'h1', { className: 'greeting' }, 'Привет, мир!' );

Синтаксис JSX необязателен, однако способствует читаемости кода, так как очень похож на HTML, но более строгий к синтаксису.

JSX позволяет вкладывать в него JavaScript с помощью фигурных скобок. Значения атрибутов в JSX также могут быть JavaScript выражениями, для этого вместо кавычек необходимо использовать фигурные скобки.

Написание тегов

Необходимо всегда закрывать одинарные теги, такие как <br />.

Компонент не может возвращать несколько JSX тегов. Необходимо обернуть их в общий родительский или использовать пустую обертку <>...</> (React Fragment).

Установка атрибутов

Установка атрибутов с помощью JSX происходит в стиле camelCase:

const element = <div tabIndex="0"></div>;
Написание стилей

Инлайн стили можно передать в виде объекта со свойствами в camelCase:

<div style={{ backgroundColor: 'black', color: 'red'}}></div>

Еще больше о JSX

Логические значения, null и undefined игнорируются

Эти выражения будут представлены одной и той же строкой:

<div /> <div></div> <div>{false}</div> <div>{null}</div> <div>{undefined}</div> <div>{true}</div>

Для того, чтобы вывести их явно, необходимо использовать преобразование в строку:

<div> Моя переменная JavaScript: {String(myVariable)}. </div>
Строковые литералы

JSX удаляет пробел в начале и конце строки, также удаляя пустые строки, а новые линии в середине строки конвертируются в единственный пробел.

Следующие два выражения эквивалентны:

<div> Hello World </div> <div>Hello World</div>

Свойства props

Когда React видит элемент, представляющий пользовательский компонент, он передаёт JSX-атрибуты этому компоненту в виде единственного объекта, называемого props. Свойства объекта props доступны только для чтения.

export default function App() { return ( <div> <Welcome name="Сара" /> <Welcome name="Эдит" /> </div> ); } function Welcome(props) { return <h1>Привет, {props.name}</h1>; }
Свойство children

В свойстве children содержатся вложенные в компонент элементы, о которых сам компонент может не знать.

export default function App() { return ( <div> <Welcome name="Сара">Привет,</Welcome> <Welcome name="Эдит">Привет,</Welcome> </div> ); } function Welcome(props) { return <h1>{props.children} {props.name}</h1>; }
Свойства по умолчанию

Если не передать значение в JSX, оно по умолчанию будет true. Следующие два выражения эквивалентны:

<MyTextBox autocomplete /> <MyTextBox autocomplete={true} />

Отрисовка по условию

Для написания условий используется синтаксис JavaScript: if..else, тернарный оператор, логическое И.

let content; if (isLoggedIn) { content = <AdminPanel />; } else { content = <LoginForm />; } return ( <div> {content} </div> );

Отрисовка списка

Элемент списка должен иметь атрибут key c уникальным значением. Значение атрибута key должно быть уникальным только среди соседних элементов, а не глобально. Лучше всего для этого подойдет id элемента..

Чтобы для каждого элемента списка отобразить несколько DOM-узлов, необходимо явно импортировать Fragment (сокращенный синтаксис <></> не позволяет указывать атрибут key):

import { Fragment } from 'react'; // ... const listItems = people.map(person => <Fragment key={person.id}> <h1>{person.name}</h1> <p>{person.bio}</p> </Fragment> );

Чистые компоненты

Некоторые JavaScript функции называются чистыми. Чистые функции:

  1. Не изменяют переменные снаружи функции.
  2. Для одинаковых входных данных результат работы функции будет неизменным.
  3. Компоненты, написанные как чистые функции, позволяют избежать многих ошибок и непредсказуемого поведения по мере роста приложения.

    Например, если требуется использовать методы push, pop, reverse, sort для данных в props, необходимо сначала сделать копию этих данных.

    Обновление отрисованного элемента

    React-элементы неизменяемы. Создав однажды элемент, нельзя изменить его дочерние элементы или атрибуты. Единственный способ обновить интерфейс — создать новый элемент и передать его в ReactDOM.render().

    function tick() { const element = ( <div> <h1>Привет, мир!</h1> <h2>Сейчас {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.createRoot(document.getElementById('root')).render(element); } setInterval(tick, 1000);

    На практике большинство приложений React только один раз вызывают метод render().

    React обновляет только то, что необходимо

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

Создание приложения

Чтобы начать использовать React, можно воспользоваться готовыми утилитами или выполнить настройку проекта вручную.

Create React App

npx create-react-app <project-directory>

npm start запускает локальный сервер.

Использование с Typescript
npx create-react-app <project-directory> --template typescript

Vite

npm create vite@latest

Далее перейти в папку с проектом cd <project-directory> и установить зависимости npm install.

npm run dev запускает локальный сервер.

Настройка вручную

Пример пошаговой настройки проекта вручную:

  1. npm init создать package.json.
  2. npm i react react-dom установить необходимые пакеты.
  3. npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin установить пакеты для разработки.
  4. Добавить в package.json скрипты: "dev": "webpack serve --mode development", "build": "webpack --mode production",
  5. Создать в корне проекта папку src, а в ней файл index.js.
  6. В index.js написать: import React from 'react' import ReactDOM from 'react-dom/client' ReactDOM.createRoot(document.getElementById('root')).render(<div>Контент</div>)
  7. Создать в корне папку public, а в ней index.html.
  8. В index.html создать структуру страницы и добавить корневой элемент <div id="root"></div>.
  9. npm i -D @babel/core @babel/preset-env @babel/preset-react babel-loader установить Babel.
  10. Создать в корне проекта файл конфига .babelrc с содержимым: { "presets": ["@babel/preset-env", "@babel/preset-react"] }
  11. Создать в корне проекта файл конфига webpack.config.js c содержимым: const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require('path'); module.exports = { mode: "development", entry: "./src/index.js", output: { filename: 'bundle.js', }, module: { rules: [ { test: /\.(jsx|js)$/, exclude: /node_modules/, use: { loader: "babel-loader", }, }, { test: /\.css$/, use: [ "style-loader", { loader: "css-loader", options: { importLoaders: 1, modules: true, }, }, ], }, ], }, resolve: { extensions: [".js", ".jsx"], }, plugins: [ new HtmlWebpackPlugin({ template: "./public/index.html", }), ], devServer: { port: 3000, static: { directory: path.join(__dirname, "dist"), }, hot: true, open: true, }, };
  12. npm i -D style-loader css-loader добавить поддержку CSS-модулей (использование: import * as classes from './<file>.module.css').
  13. Добавить файл .gitignore с содержимым: /node_modules/ /dist/

Деплой на github pages

Create React App

Последовательность действий:

  1. npm install gh-pages --save-dev
  2. Добавить в package.json: "homepage": "https://<username>.github.io/<repo>/", ... "scripts": { "predeploy": "npm run build", "deploy": "gh-pages -d dist", ... }
  3. Выполнить команду npm run deploy.
React Router

При использовании библиотеки react-router-dom следует заменить BrowserRouter на HashRouter.

Vite app

Добавить base: "/<repo>/", в vite.config.js, затем выполнить те же шаги, что и для Create React App.

Добавление интерактивности

Responding to events

Naming event handler props

Принято соглашение, по которому props, в которые передаются обработчики событий, следует именовать с префиксом on, после которого с заглавной буквы идет название свойства, например, onPlayMovie.

Capture phase events

Чтобы назначить обработчик в фазе погружения (Capturing phase), необходимо в конец имени события дописать Capture.

export default function App() { function handleClick(e) { e.stopPropagation(); console.log('BUTTON'); } return ( <div onClickCapture={() => console.log('DIV') /* этот обработчик запустится раньше */}> <button onClick={handleClick}>Клик!</button> </div> ); }

State: A Component's Memory

Когда обычной переменной недостаточно

Локальных переменных часто может быть недостаточно:

  1. Локальные переменные не сохраняются между рендерингами.
  2. Когда React повторно рендерит компонент, он рендерит его с нуля — не учитывая изменения в локальных переменных.

Чтобы обновить данные компонента, необходимо сделать две вещи:

  1. Сохранить данные между рендерингами.
  2. Заставить React выполнить рендеринг компонента с новыми данными.

Хук useState решает обе этих задачи, предоставляя:

  1. Переменную state для сохранения данных между рендерингами.
  2. Фукнцию для обновления state и запуска нового рендеринга.

Первый хук

В React useState, как и любая другая функция, начинающаяся с use, называется хуком.

Хуки позволяют использовать состояние и другие возможности React, не объявляя класс. Внутри классов они не работают.

Хуки можно использовать только на верхнем уровне компонентов (или кастомных хуков). React полагается на порядок вызова хуков.

Чтобы использовать хук, его необходимо импортировать в начале файла:

import { useState } from 'react';

Добавление состояния

Синтаксис:

const [state, setState] = useState(initialState)

state - состояние компонента.

setState - функция, которая обновляет состояние и запускает новый рендеринг. Принимает новое значение state.

initialState - state при первом рендеринге. Если initialState - функция, в state будет записан результат ее вызова.

Пример
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => setNumber(number + 1)}>+1</button> </> ); }
Обновление объектов в state

Если в state находится объект, то вместо того, чтобы менять его свойства, необходимо создать новый объект на основе предыдущего.

Очередь обновлений состояния

Изменение state запланирует новый рендеринг. Однако, иногда необходимо выполнить несколько изменений state сразу.

Это можно сделать, передав в setState функцию. Она получает старое значение state и должна вовращать новое:

import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); function handleClick() { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); } return ( <> <h1>{number}</h1> <button onClick={handleClick}>+3</button> </> ); }
Обновления состояния объединяются

React может выполнять несколько вызовов setState за одно обновление для лучшей производительности .

Изолированность состояния

Состояние является локальным для каждого компонента. Если отобразить один и тот же компонент дважды, каждая копия будет иметь полностью изолированный state. Изменение state одного компонента не повлияет на state другого компонента.

export default function App() { return ( <div> <Counter /> <Counter /> </div> ); }

Поднятие состояния

Общий state у разных компонентов достигается путём его перемещения до ближайшего общего предка и передачи state обратно в качестве props.

State as a Snapshot

Значение state никогда не изменяется в пределах рендеринга, даже если код, который запрашивает state, асинхронный:

import { useState } from 'react'; export default function App() { const [number, setNumber] = useState(0); function handleClick() { setNumber(number + 5); setTimeout(() => console.log(number), 2000); // 0 } return ( <> <div>Число: {number}</div> <button onClick={handleClick}>Клик!</button> </> ); }

Управление состоянием

Preserving and Resetting State

Сохранение состояния

Состояние компонента сохраняется при рендеринге компонента на том же месте в дереве элементов, но с другими props.

Сброс состояния

Состояние компонента сбрасывается при:

  1. Удалении компонента.
  2. Рендеринге компонента на другом месте в дереве элементов.
  3. Рендеринге компонента с другим ключом key.

Extracting State Logic into a Reducer

Хуки useState и useReducer используются для управления состоянием и делают одно и то же.

Отличия синтаксиса useState от useReducer:

const [state, setState] = useState(initialState); const [state, dispatch] = useReducer(reducer, initialState);

dispatch - функция, которая принимает изменения action в качестве единственного аргумента.

reducer - функция, которая принимает prevState и action. Возвращает новый state.

  • prevState - старое состояние state
  • action - изменения, который отправлены с помощью dispatch
useState -> useReducer

useReducer - иной способ управления состоянием. Чтобы перейти от использования useState к useReducer необходимо:

  1. Вместо setState использовать dispatch, передав в качестве аргумента изменения action.
  2. Написать reducer функцию, которая на основе prevState и action возвращает новый state.
  3. Заменить useState на useReducer.
useReducer изнутри

Внутреннее устройство useReducer выглядит примерно так:

function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); function dispatch(action) { setState(state => reducer(state, action)); } return [state, dispatch]; }
Пример
import { useState, useReducer } from 'react'; let nextId = 2; const initialTasks = [ { id: 0, text: 'Visit Kafka Museum' }, { id: 1, text: 'Watch a puppet show' }, ]; export default function TaskApp() { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId, }); } return ( <> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [ ...tasks, { id: action.id, text: action.text, }, ]; } case 'deleted': { return tasks.filter((t) => t.id !== action.id); } } } function AddTask({ onAddTask }) { const [input, setInput] = useState(''); return ( <div> <input value={input} onChange={e => setInput(e.target.value)} /> <button onClick={() => { setInput(''); onAddTask(input) }}>Add</button> </div> ); } function TaskList({ tasks, onDeleteTask }) { return ( <div> {tasks.map(task => <div key={task.id}> {task.text} <button onClick={() => onDeleteTask(task.id)}>Delete</button> </div> )} </div> ); }

Passing Data Deeply with Context

Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.

Для передачи контекста необходимо:

  1. Создать и экспортировать его: export const MyContext = createContext(defaultValue).
  2. Передать его в useContext(MyContext) в любом дочернем компоненте, независимо от того, насколько глубоко он находится.
  3. Обернуть дочерние компоненты в <MyContext.Provider value={...}>, чтобы они могли использовать контекст.
Пример
import { useState, createContext, useContext } from 'react'; const places = [ { id: 0, name: 'Bo-Kaap', imageId: 'K9HVAGH' }, { id: 1, name: 'Rainbow Village', imageId: '9EAYZrt'}, { id: 2, name: 'Macromural de Pachuca', imageId: 'DgXHVwu'}, ]; const ImageSizeContext = createContext(500); export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <ImageSizeContext.Provider value={imageSize}> <label> <input type="checkbox" checked={isLarge} onChange={e => setIsLarge(e.target.checked)} /> Use large images </label> <List /> </ImageSizeContext.Provider> ) } function List() { const listItems = places.map(place => <li key={place.id}> <Place place={place} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place }) { return ( <> <p>{place.name}</p> <PlaceImage place={place} /> </> ); } function PlaceImage({ place }) { const imageSize = useContext(ImageSizeContext); return ( <img src={`https://i.imgur.com/${place.imageId}l.jpg`} alt={place.name} width={imageSize} height={imageSize} /> ); }

Scaling Up with Reducer and Context

Чтобы объединить Reducer с Context, необходимо:

  1. Создать state и dispatch контекст.
  2. Предоставить контекст с помощью Context.Provider.
  3. Использовать контекст в любом дочернем компоненте.
Пример
import { useState, createContext, useContext, useReducer } from 'react'; const TasksContext = createContext(null); const TasksDispatchContext = createContext(null); let nextId = 2; const initialTasks = [ { id: 0, text: 'Philosopher’s Path' }, { id: 1, text: 'Visit the temple' }, ]; export default function TaskApp() { return ( <TasksProvider> <AddTask /> <TaskList /> </TasksProvider> ); } function TasksProvider({ children }) { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> {children} </TasksDispatchContext.Provider> </TasksContext.Provider> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'deleted': { return tasks.filter(t => t.id !== action.id); } } } function AddTask() { const [text, setText] = useState(''); const dispatch = useContext(TasksDispatchContext); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => { setText(''); dispatch({ type: 'added', id: nextId++, text: text }); }}>Add</button> </> ); } function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const dispatch = useContext(TasksDispatchContext); return ( <label> {task.text} <button onClick={() => dispatch({ type: 'deleted', id: task.id })}> Delete </button> </label> ); }

Escape hatches

Referencing Values with Refs

Хук useRef позволяет компоненту запоминать какую-либо информацию, не запуская новый рендеринг при ее изменении.

Синтаксис:

const ref = useRef(initialValue)

ref - объект вида { current: initialValue }.

Если необходимо читать или записывать в ref.current во время рендеринга, то вместо этого лучше использовать state.

Пример
import { useState, useRef } from 'react'; export default function App() { const [counter, setCounter] = useState(0); const ref = useRef(0); return ( <> <button onClick={() => setCounter(counter + 1)}>{counter}</button> <button onClick={() => ref.current++}>Ref: {ref.current}</button> </> ) }
Использование

Хук useRef используется в следующих случаях:

  1. Хранение идентификаторов timerId.
  2. Хранение элементов DOM и управление ими.
  3. Хранение других объектов, которые не нужны для вычисления JSX.
useRef изнутри

Фунциональность useRef можно реализовать при помощи useState, создав кастомный хук:

function useRef(initialValue) { const [ref, unused] = useState({current: initialValue}); return ref; };

Manipulating the DOM with Refs

С помощью атрибута ref можно сохранить ссылку на элемент в myRef.current.

Пример
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}>Focus the input</button> </> ); }

Accessing another component’s DOM nodes

Атрибут ref можно передавать из родительского компонента в дочерние точно так же, как и остальные атрибуты.

Пример
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}>Focus the input</button> </> ); } function MyInput({ ref }) { return <input ref={ref} />; }

В примере выше поскольку <input> является встроенным DOM-элементом, React сохраняет ссылку на него в ref.current.

В старых версиях React для передачи ref дочернему компоненту использовался forwardRef. С выходом React 19 этот API считается устаревшим и необходимости в его использовании больше нет.

С помощью атрибута ref можно легко получать ссылки на встроенные компоненты. Если же передать атрибут ref другому компоненту, в myRef.current запишется null, потому что элемент явно не указан.

Компонент, которому передан атрибут ref, должен явно указать элемент, который будет записан в myRef.current. Для этого он:

  1. Должен быть объявлен с помощью forwardRef, получив myRef вторым аргументом.
  2. Передать myRef нужному элементу.
Пример
import { useRef, forwardRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}>Focus the input</button> </> ); } const MyInput = forwardRef((props, ref) => { return <input ref={ref} />; });
Exposing a subset of the API with an imperative handle

С помощью хука useImperativeHandle можно предоставлять только необходимую часть функциональности для взаимодействия с элементом.

Flushing state updates synchronously with flushSync

Функция flushSync дает команду React обновить DOM сразу после выполнения кода, заключенного в flushSync.

Пример
import { useState, useRef } from 'react'; import { flushSync } from 'react-dom'; export default function TodoList() { const listRef = useRef(null); const [list, setList] = useState(initialList); function handleAdd() { const newTodo = getNextItem(); flushSync(() => { setList([...list, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth' }); } return ( <> <button onClick={handleAdd}>Add</button> <ul ref={listRef}> {list.map(item => ( <li key={item.id}>{item.text}</li> ))} </ul> </> ); } let nextId = 0; let initialList = []; const getNextItem = () => ({ id: nextId++, text: `Item #${nextId}`}); for (let i = 0; i < 25; i++) { initialList.push(getNextItem()); }

How to manage a list of refs using a ref callback

Чтобы получить ссылку на каждый элемент списка, длина которого неизвестна, следует передавать в атрибут ref callback. Callback будет запускаться при рендеринге со значением элемента и при удалении из React DOM со значением null.

Пример
import { useRef, useState } from "react"; export default function CatFriends() { const itemsRef = useRef(new Map()); const [catList, setCatList] = useState(setupCatList); function scrollToCat(cat) { const node = itemsRef.current.get(cat); node.scrollIntoView({ behavior: "smooth", inline: "center" }); } return ( <> <nav> <button onClick={() => scrollToCat(catList[0])}>First Neo</button> <button onClick={() => scrollToCat(catList[5])}>Millie</button> <button onClick={() => scrollToCat(catList[9])}>Bella</button> </nav> <div style={{ display: 'flex', overflow: 'auto' }}> {catList.map(cat => ( <img key={cat} src={cat} ref={node => { const map = itemsRef.current; if (node) { map.set(cat, node); } else { map.delete(cat); } }} /> ))} </div> </> ); } function setupCatList() { const catList = []; for (let i = 0; i < 10; i++) { let catName = 'neo'; if (i === 5) { catName = 'millie'; } else if (i === 9) { catName = 'bella'; } catList.push(`https://placecats.com/${catName}/250/200#${i}`); } return catList; }

Synchronizing with Effects

Хук useEffect позволяет запускать некоторый код после рендеринга для того, чтобы синхронизировать свой компонент с какой-либо системой за пределами React.

Синтаксис:

useEffect(fn, dependencies?)

fn - функция, которая будет вызвана после рендеринга.

dependencies - необязательный аргумент с массивом зависимостей.

При изменении любой из зависимостей (сравнение происходит с помощью Object.is) fn будет вызвана заново. Если dependencies не указан, fn будет запускаться после каждого рендеринга. Если dependencies - пустой массив, fn запустится только после первоначального рендеринга.

Из useEffect можно вернуть функцию очистки. Она будет вызвана либо перед следующим срабатыванием useEffect, либо при удалении компонента из React DOM.

Пример
import { useEffect } from 'react'; function createConnection() { // A real implementation would actually connect to the server return { connect() { console.log('✅ Connected'); }, disconnect() { console.log('❌ Disconnected'); } }; } export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
Глобальные переменные

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

Если необходимо узнавать об изменении внешней переменной, стоит использовать хук useSyncExternalStore.

Выполнение асинхронных запросов

С помощью useEffect можно отправлять fetch-запросы, но необходимо предусмотреть отмену:

useEffect(() => { let ignore = false; fetchResults(query, page).then(json => { if (!ignore) { setResults(json); } }); return () => { ignore = true; }; }, [query, page]);

Когда useEffect не нужен

Если можно что-то вычислить во время рендеринга, useEffect не нужен.

Код, который выполняется не из-за отображения компонента, должен находиться в обработчиках событий.

Hooks

useId

Хук useId используется для создания уникальных идентификаторов.

const id = useId()

Использование:

  1. Создание уникальных идентификаторов для атрибутов доступности (a11y).
  2. Создание идентификаторов для нескольких связанных элементов.
  3. Использование одинаковых id на клиенте и на сервере.
Пример
import { useId } from 'react'; function PasswordField() { const passwordHintId = useId(); return ( <> <label> Password: <input type="password" aria-describedby={passwordHintId} /> </label> <p id={passwordHintId}> The password should contain at least 8 characters </p> </> ); } export default function App() { return ( <> <h2>Choose password</h2> <PasswordField /> <h2>Confirm password</h2> <PasswordField /> </> ); }

useLayoutEffect

Хук useLayoutEffect - аналог useEffect, но позволяет получить доступ к элементу до его отрисовки браузером.

useLayoutEffect(fn, dependencies?)

React гарантирует, что код внутри useLayoutEffect и любые обновления состояния, запланированные внутри него, будут обработаны до того, как браузер перерисует экран.

Это позволяет отобразить элемент на экране, измерить его и повторно отобразить так, чтобы пользователь не заметил первого отображения. Другими словами, useLayoutEffect блокирует отображение в браузере.

Может негативно сказываться на производительности, поэтому, если возможно, лучше использовать useEffect.

Использование:

  1. Получение размеров элемента до обновления DOM.
Пример
import { useRef, useLayoutEffect } from 'react'; export default function ScrollToBottom() { const ref = useRef(null); useLayoutEffect(() => { ref.current.scrollTo({ top: ref.current.scrollHeight }); }, []) return ( <div ref={ref} style={{ height: 100, width: 100, overflow: 'auto' }}> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Non optio eum tempore amet nulla? </div> ); };

useMemo

Хук useMemo позволяет сократить число вызовов функции.

const savedValue = useMemo(fn, dependencies)

fn - функция без аргументов. Вызывается только при изменении dependencies.

savedValue - значение, которое вернула fn.

Использование:

  1. Пропуск дорогостоящих повторных вычислений.
  2. Кеширование зависимости другого хука.
Пример
function TodoList({ todos, filter, theme }) { const visibleTodos = useMemo(() => { return getFilteredTodos(todos, filter); }, [todos, filter]); return ( <div className={theme}> <List items={visibleTodos} /> </div> ); }

useCallback

Хук useCallback позволяет сохранять одну и ту же функцию между ререндерингами.

const cachedFn = useCallback(fn, dependencies)

fn - функция, которую необходимо кешировать.

cachedFn - кешированная версия fn.

Использование:

  1. Предотвращение слишком частого срабатывания эффекта.
  2. Оптимизация кастомного хука. Рекомендуется обернуть все функции, которые возвращает кастомный хук, в useCallback.

Выражения useCallback(fn, deps) и useMemo(() => fn, deps) эквивалентны.

Пример
function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback(orderDetails => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); }

useImperativeHandle

Хук useImperativeHandle позволяет настроить предоставляемый в ref функционал.

useImperativeHandle(ref, createHandle, dependencies?)

ref - ссылка, которую нужно изменить.

createHandle - функция без аргументов, которая возвращает то, что будет записано в ref.current.

dependencies - необязательный аргумент с массивом зависимостей.

Использование:

  1. Предоставление только определенных свойств элемента внешнему компоненту.
  2. Предоставление кастомных императивных методов.
Пример
import { useRef, useImperativeHandle } from 'react'; export default function Form() { const inputRef = useRef(null); return ( <> <MyInput ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> <button onClick={() => inputRef.current.remove() /* Ошибка */}>Remove</button> </> ); } function MyInput({ ref }) { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ focus() { realInputRef.current.focus(); }, })); return <input ref={realInputRef} />; };

useDebugValue

Хук useDebugValue позволяет отобразить метку для кастомного хука в React Dev Tools.

useDebugValue(value, format?)

value - значение, которое нужно отобразить.

format - необязательная функция, которая вызывается только при инспектировании компонента. Принимает value и возвращает значение, которое будет отображено в итоге. Позволяет избежать потенциально дорогостоящей операции форматирования до того, как компонент будет проинспектирован в React Dev Tools.

Использование:

  1. Упрощение отладки кастомного хука в React Dev Tools.
Пример
function useCustomHook() { const [name, setName] = useState(''); useDebugValue(name ? 'name задано' : 'name не задано'); return {name, setName}; }

useDeferredValue

Хук useDeferredValue позволяет отложить обновление части UI.

const deferredValue = useDeferredValue(value)

value - значение, которое нужно отложить.

Во время первоначального рендеринга deferredValue будет таким же, как и value.

Во время обновлений deferredValue будет “отставать” от последнего значения. В частности, React сначала выполнит рендеринг без обновления deferredValue, а затем попытается выполнить рендеринг с вновь полученным значением в фоновом режиме.

Использование:

  1. Отображение резервного контента во время загрузки содержимого.
  2. Отображение устаревшего контента во время загрузки нового.
Пример
import { useState, useDeferredValue, memo } from 'react'; export default function App() { const [text, setText] = useState(''); const deferredText = useDeferredValue(text); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <SlowList text={deferredText} /> </> ); } const SlowList = memo(function SlowList({ text }) { let items = []; for (let i = 0; i < 25; i++) { items.push(<SlowItem key={i} text={text} />); } return ( <ul>{items}</ul> ); }); function SlowItem({ text }) { let startTime = performance.now(); while (performance.now() - startTime < 10) { // Do nothing for 10 ms per item to emulate extremely slow code } return ( <li>Text: {text}</li> ); }

useTransition

Хук useTransition позволяет обновлять состояние не блокируя UI.

const [isPending, startTransition] = useTransition()

isPending - флаг, который говорит, есть ли в данный момент transition.

startTransition - функция, которая позволяет начать transition. Принимает один параметр - функцию без аргументов, которая обновляет state посредством вызова одной или нескольких setState функций.

Использование:

  1. Отложенное обновление состояний, которые не являются срочными для того, чтобы не блокировать UI.
  2. Создание отзывчивого UI даже на медленных устройствах.
Пример
import { useState, useTransition, memo } from 'react'; export default function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); function selectTab(nextTab) { startTransition(() => setTab(nextTab)); } return ( <> <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')} > About </TabButton> <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')} > Posts (slow) </TabButton> <TabButton isActive={tab === 'contact'} onClick={() => selectTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </> ); } function TabButton({ children, isActive, onClick }) { if (isActive) { return <b>{children}</b>; } return ( <button onClick={onClick}> {children} </button> ); } function AboutTab() { return ( <p>Welcome to my profile!</p> ); } const PostsTab = memo(function PostsTab() { let items = []; for (let i = 0; i < 50; i++) { items.push(<SlowPost key={i} index={i} />); } return ( <ul>{items}</ul> ); }); function SlowPost({ index }) { let startTime = performance.now(); while (performance.now() - startTime < 10) { // Do nothing for 10 ms per item to emulate extremely slow code } return ( <li>Post #{index + 1}</li> ); } function ContactTab() { return ( <> <p>You can find me online here:</p> <ul> <li>admin@mysite.com</li> <li>+123456789</li> </ul> </> ); }

useActionState

Хук useActionState позволяет управлять состоянием на основе результата отправки формы.

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?)

fn - функция, которая будет вызвана при отправке формы. Первым аргументом получает предыдущее значение state. Вторым аргументом получает объект FormData, содержащий данные отправленной формы. Возвращаемое значение функции станет новым значением state.

initialState - начальное значение state формы. Должно быть сериализуемым (сериализуемое значение можно преобразовать в строку с помощью JSON.stringify()).

permalink - строка, содержащая уникальный URL страницы, которую изменяет эта форма.

state - результат последнего вызова fn. Изначально равен initialState.

formAction - функция, которую нужно передать в качестве атрибута action элементу <form> или атрибута formAction кнопке <button> внутри формы.

isPending - флаг, который сообщает, выполняется ли в данный момент отправка формы. Равен true, если форма была отправлена, но выполнение fn не было завершено, иначе false.

Пример
import { useActionState } from "react"; export default function App() { return ( <> <AddToCartForm itemID="1" itemTitle="JavaScript: The Definitive Guide" /> <AddToCartForm itemID="2" itemTitle="JavaScript: The Good Parts" /> </> ) } function AddToCartForm({ itemID, itemTitle }) { const [message, formAction, isPending] = useActionState(addToCart, null); return ( <form action={formAction}> <h2>{itemTitle}</h2> <input type="hidden" name="itemID" value={itemID} /> <button type="submit">Add to Cart</button> {isPending ? "Loading..." : message} </form> ); } async function addToCart(prevState, queryData) { "use server"; const itemID = queryData.get('itemID'); if (itemID === "1") { return "Added to cart"; } else { await new Promise(resolve => { setTimeout(resolve, 2000); }); // Add a fake delay to make waiting noticeable. return "Couldn't add to cart: the item is sold out."; } }

useInsertionEffect

Хук useInsertionEffect используется для вставки стилей авторами CSS-in-JS библиотек.

useInsertionEffect(fn, dependencies?)

fn - вызывается при добавлении компонента в DOM, но до запуска layout-эффектов. Может возвращать функцию очистки.

Использование:

  1. Вставка элементов в DOM до запуска layout-эффектов.
  2. Внедрение динамических стилей из CSS-in-JS библиотек.

Особенности:

  1. Хук useInsertionEffect не должен обновлять state.
  2. Когда useInsertionEffect выполняется, refs еще не связаны с элементами.
  3. Хук предназначен для использования библиотеками, а не кодом приложения.

useSyncExternalStore

Хук useSyncExternalStore позволяет отслеживать изменение данных во внешнем хранилище, например, в браузере.

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

subscribe - функция, которая принимает единственный аргумент - callback, предоставляемый самим React. В subscribe необходимо подписать callback на соответствующее событие и вернуть функцию отмены от подписки.

getSnapshot - функция, которая возвращает данные из внешнего хранилища. Если хранилище изменяется и возвращаемое значение отличается, React заново рендерит компонент.

getServerSnapshot - необязательная функция, которая возвращает первоначальное состояние данных в хранилище. Без этого аргумента рендеринг компонента на стороне сервера выбросит ошибку.

Использование:

  1. Позволяет подписаться на некоторое изменяющееся значение, предоставлямое внешним хранилищем.
Пример
import { useCallback, useSyncExternalStore } from 'react'; function useMediaQuery(query) { const subscribeMediaQuery = useCallback(onChange => { const mql = window.matchMedia(query); mql.addEventListener("change", onChange); return () => mql.removeEventListener("change", onChange); }, [query]); const matches = useSyncExternalStore( subscribeMediaQuery, () => window.matchMedia(query).matches ); return matches; } export default function App() { const matches = useMediaQuery("(max-width: 991px)"); return <div>{'matches: ' + matches}</div>; }

Больше об использовании useSyncExternalStore можно узнать в этой статье.

useEffectEvent

Хук useEffectEvent используется в эффекте в тех случаях, когда необходимо избежать повторного запуска эффекта при изменении props и state, но в то же время получить их актуальные значения.

const onSomething = useEffectEvent(callback)

callback - функция, которой необходимо получить доступ к последним значениям props и state при вызове.

onSomething - функция, которая должна быть вызвана внутри useEffect, useLayoutEffect или useInsertionEffect.

Добавление новой зависимости в эффект приведет к повторному запуску эффекта при изменении значения этой зависимости. Если перезапуск эффекта нежелателен, но требуется получить новое значение props или state, следует использовать хук useEffectEvent.

Пример
import { useState, useEffect, useEffectEvent } from 'react'; export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)}> <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); } function ChatRoom({ roomId, theme }) { const showMessage = useEffectEvent(message => { console.log(`🔔 Уведомление (тема: "${theme}"): ${message}`); }); useEffect(() => { const connection = createConnection(roomId); connection.onMessage((message) => { showMessage(message); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Комната: {roomId} (Тема: {theme})</h1>; } function createConnection(roomId) { let messageCallback = null; let intervalId; return { connect() { console.log(`✅ Подключение к комнате "${roomId}"`); intervalId = setInterval(() => { if (messageCallback) messageCallback(`Новое сообщение в ${roomId}`); }, 3000); }, onMessage(callback) { messageCallback = callback; }, disconnect() { console.log(`❌ Отключение от комнаты "${roomId}"`); clearInterval(intervalId); } }; }

Разное

Reconciliation

Reconciliation - это алгоритм, с помощью которого React определяет наиболее эффективный способ обновления UI в результате изменения данных.

Метод render() создает дерево React-элементов. При следующем обновлении state или props, render() возвращает другое дерево React-элементов. Далее React должен выяснить, как эффективно обновить UI для соответствия последнему дереву элементов.

Существуют общие решения для алгоритмической проблемы вычисления минимального количества операций по преобразованию одного дерева в другое, однако сложность такого алгоритма O(n^3), где n - количество элементов дерева. Поэтому React использует вместо этого эвристический алгоритм со сложностью O(n), основанный на двух допущениях:

  1. Два элемента разных типов создают разные деревья.
  2. Разработчик может намекнуть, какие дочерние элементы не будут меняться при рендере с помощью свойства key.
Элементы разных типов

Если корневые элементы имеют разные типы (например, p и div) React сотрет старое дерево и создаст новое с нуля. При стирании дерева, старые DOM узлы и связанный с предыдущим деревом state удаляются.

DOM элементы одного типа

При сравнении двух элементов одного типа React смотрит на атрибуты каждого и обновляет только их.

Рекурсия по дочерним элементами

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

Ключи

React поддерживает атрибут key. Если у дочерних элементов он есть, React использует его для эффективного сравнения и изменения двух деревьев.

Strict mode

Строгий режим в React - это инструмент для подсвечивания потенциальных проблем при написании кода.

Для активации строгого режима достаточно обернуть код в тег StrictMode (или React.StrictMode), он будет активирован только для вложенных в него дочерних элементов. Строгий режим не влияет на production сборку.

Функции, которые выполняет строгий режим:

  1. Идентификация компонентов с небезопасными жизненными циклами.
  2. Обнаружение неожиданных побочных эффектов.
  3. Предупреждение об использовании устаревших API.
Строгий режим в процессе разработки

В строгом режиме React повторно рендерит каждый компонент (state и DOM-дерево сохраняются). Это помогает находить эффекты, требующие очистки, и выявлять ошибки на ранней стадии. Кроме того, при сохранении файла в процессе разработки, React повторно рендерит используемые в нем компоненты.

Error boundaries

Error boundaries работают только внутри классовых компонентов.

Простейший классовый компонент:

class Welcome extends React.Component { render() { return <h1>Привет, {this.props.name}</h1>; } }

Error boundaries - возможность ловить ошибки JavaScript внутри их дочерних компонентов и отображать интерфейс, предназначенный для этого.

Классовый компонент становится Error boundary если определяет как минимум один из методов жизненного цикла:

  • static getDerivedStateFromError() - отображает резервный интерфейс
  • componentDidCatch() - выводит информацию

Библиотеки для React

React Router

React Router - библиотека для маршрутизации в React.

npm i react-router-dom - установка.

Использование

Чтобы начать использовать React Router, необходимо выполнить следующие шаги:

  1. Настроить роутер. Достаточно обернуть все приложение в компонент <BroserRouter />: ReactDOM.createRoot(document.getElementById('root')).render( <BrowserRouter> <App /> </BrowserRouter> );
  2. Прописать маршруты. Добавить компонент <Routes />, а в нем дочерние <Route /> со свойствами path и element: <Routes> <Route path="/" element={<Home />} /> <Route path="/cart" element={<Cart />} /> <Route path="*" element={<NotFound />} /> </Routes>
  3. Управление навигацией. Заменить ссылки a на компоненты <Link /> со свойством to: <nav> <ul> <li><Link to="/">Home</Link></li> <li><Link to="/cart">Cart</Link></li> </ul> </nav>

Хуки

useNavigate

Хук useNavigate возвращает функцию, которую можно использовать для программной навигации (в императивном ключе, в отличии навигации с помощью компонента <Link />).

function Info() { const navigate = useNavigate(); return <button onClick={() => navigate("/")}>Вернуться назад</button>; }
useLocation

Хук useLocation возвращает текущий объект window.location.

При изменении location происходит новый рендеринг компонента, использующего этот хук.

function Header() { const location = useLocation(); return ( <header> <Logo /> {location.pathname === '/' && <Search />} </header> ) }
useParams

Хук useParams возвращает объект, содержащий динамические параметры текущего URL-адреса, заданные с помощью свойства path компонента <Route />.

function App() { return ( <Routes> <Route path="/" element={<Home />} /> <Route path="/:id" element={<Product />} /> </Routes> ); } function Product() { const params = useParams(); return <h2>Product #{params.id}</h2>; }

Компоненты

Outlet

Компонент <Outlet /> следует использовать в родительских <Route /> элементах.

На месте <Outlet /> будет рендериться дочерний <Route />, что позволяет писать вложенный UI.

function AppLayout() { return ( <> <Header /> <Outlet /> </> ); } function App() { return ( <Routes> <Route path="/" element={<AppLayout />}> <Route path="" element={<Home />} /> <Route path="cart" element={<Cart />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> ); }