Существует множество способов структурировать код так, чтобы он был читабельным, и у каждого свой подход к этому. Возникает вопрос, есть ли лучший способ сделать это?
Мы поговорим о лучших практиках:
- Организация каталога
- Компоненты и разделение ответственности
- Работа с состояниями
- Абстракция
- Соглашения об именовании
1. Организация каталогов
В документации React упоминается, что в целом существует два основных способа организации вашего приложения React: Группировка по функциям или маршрутам и Группировка по типу файлов. Главное здесь - не перемудрить.
Если вы начинаете с небольшого приложения, вы можете органично организовать свой код по ходу работы так, как вам удобно. Помните, что React не имеет собственной жесткой структуры, поэтому на 100% зависит от вас, как все будет структурировано. Если у вас есть логическое объяснение тому, как организованы файлы, то все хорошо.
Однако, поскольку в документации React упоминаются эти две стратегии организации, давайте рассмотрим каждую из них, чтобы понять, как они структурированы. Допустим, у нас есть приложение для электронной коммерции, в котором есть пользователь, список товаров, подробная страница товара, корзина и процесс оформления заказа.
Группировка по функциям
Корневой каталог здесь - это каталог src
, который является одним из двух базовых каталогов в вашем приложении React (второй - папка public
). В каталоге src
у нас будут основные файлы App.js
и index.js
в корне папки. Затем у нас будут вложенные каталоги для каждой из функций вашего приложения.
Ваш подход к структуре может варьироваться в зависимости от того, как все организовано: в проекте может быть больше каталогов, меньше каталогов или даже более глубокая вложенность на компоненты, стилизацию и тестирование.
src/
App.js
App.css
App.test.js
index.js
global/ <= общие для всего приложения сущности
AppContext.js
ThemeContext.js
UserContext.js
Button.js
cards/
index.js
Cards.css
Cards.js
Card.css
Card.js
Card.test.js
detailed-product/
DetailedProduct.css
DetailedProduct.js
DetailedProduct.test.js
checkout/
ReviewOrder.css
ReviewOrder.js
ReviewOrder.test.js
ShoppingCart.css
ShoppingCart.js
ShoppingCart.test.js
user/
index.js
User.css
User.js
User.test.js
Группировка по типу файлов
Корневой каталог по-прежнему является каталогом src
. Все, что будет отображаться на экране клиента, по-прежнему находится в этой папке. Как и раньше, мы будем хранить файлы App.js
и index.js
в корне этого каталога, а затем каталоги, представляющие составные части приложения: компоненты, контекст, CSS, хуки и тесты.
src/
App.js
index.js
components/
App.css
Card.js
Cards.js
ConfirmationPage.js
DetailedProduct.js
Footer.js
Navbar.js
ReviewOrder.js
Settings.js
ShoppingCart.js
User.js
context/
AppContext.js
ThemeContext.js
UserContext.js
css/
Card.css
Cards.css
ConfirmationPage.css
DetailedProduct.css
Footer.css
Navbar.css
ReviewOrder.css
Settings.css
ShoppingCart.css
User.css
hooks/
useAuth.js
useAxios.js
tests/
App.test.js
Card.test.js
Cards.test.js
ConfirmationPage.test.js
DetailedProduct.test.js
Footer.test.js
Navbar.test.js
ReviewOrder.test.js
Settings.test.js
ShoppingCart.test.js
User.test.js
Как и прежде, способ настройки проекта зависит от вашего приложения и от того, как вы хотите его реализовать. Основная структура здесь зависит от типа файла и не более того. В конечном итоге структура вашего файла должна быть сделана так, чтобы в ней было легко ориентироваться. Как вы это сделаете, зависит только от вас.
О других вариантах структурирования React приложения читайте в:
2. Компоненты и разделение ответственности
До появления React Hooks было довольно легко определить, что считается компонентом класса с состоянием, а что - презентационным функциональным компонентом. Некоторые разработчики также называли их "умными" компонентами и "глупыми" компонентами. Разумеется, умные компоненты - это те, которые несут состояние и обрабатывают логику, а глупые компоненты - это те, которые просто принимают передаваемые им пропсы.
После появления React Hooks и обновления Context API почти все можно считать функциональным компонентом, что приводит к разговору о том, когда следует разделять компоненты, содержащие локальное состояние, и компоненты, которые его не содержат, и как это делать.
В конечном счете, это зависит от вас и/или вашей команды, как построить ваш паттерн проектирования, но лучшая практика показывает, что логика и компоненты с локальным состоянием должны быть отделены от статических компонентов.
Подробнее о разделении ответственности читайте в статье Разделение ответственности в React. Как использовать контейнерные и презентационные компоненты..
3. Работа с состоянием и пропсами
Поток данных в React-приложении очень важен. Есть два способа работы с данными: использование состояния или передача состояния в виде пропсов. Давайте рассмотрим лучшие практики.
Состояние
При работе с состоянием, будь то глобально в контекстном API или локально, его нельзя изменять напрямую, переназначая свойство state с новым значением:
addOne = () => { // Так обновлять состояние нельзя
this.state.counter += 1;
}
Вместо этого при работе с состоянием в классовых компонентах используйте метод this.setState()
для обновления состояния.
import React from "react";
import "./styles.css";
class Counter extends React.Component{
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
addOne = () => {
this.setState({counter: this.state.counter + 1})
}
subtractOne = () => {
this.setState({counter: this.state.counter - 1});
}
reset = () => {
this.setState({ counter: 0 });
}
render() {
return (
<div className="App">
<h1>Simple React Counter</h1>
<h2>{this.state.counter}</h2>
<button onClick={this.addOne}> + </button>
<button onClick={this.reset}> Reset </button>
<button onClick={this.subtractOne}> - </button>
</div>
);
}
}
export default Counter;
При использовании React Hooks вы будете использовать любое название своего set
метода:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [ counter, setCounter ] = useState(0);
const addOne = () => {
setCounter(counter + 1)
}
const subtractOne = () => {
setCounter(counter - 1);
}
const reset = () => {
setCounter(0);
}
return (
<div className="App">
<h1>Simple React Counter</h1>
<h2>{counter}</h2>
<button onClick={subtractOne}> - </button>
<button onClick={reset}> Reset </button>
<button onClick={addOne}> + </button>
</div>
);
}
Пропсы
При работе с пропсами и передаче состояния другим компонентам для использования, может наступить момент, когда вам потребуется передать пропсы пяти дочерним компонентам. Такой метод передачи пропсов от родителя к ребенку на протяжении нескольких поколений называется prop drilling, и его следует избегать.
Хотя код, безусловно, будет работать, если вы будете передавать проп на много уровней вниз, он подвержен ошибкам, и поток данных может быть трудно отслеживать. Вам следует создать какой-либо паттерн проектирования для глобального управления состоянием с помощью Redux или Context API (Context API в настоящее время является более простым и предпочтительным способом).
Подробнее о вариантах передачи данных между реакт компонентами читайте в статье Как передавать данные между компонентами в ReactJS
4. Абстракция
React процветает благодаря возможности повторного использования. Когда мы говорим о лучших практиках React, часто встречается термин абстракция. Абстракция означает, что есть части большого компонента или приложения, которые могут быть изъяты, превращены в собственный функциональный компонент, а затем импортированы в более крупный компонент. Если сделать компонент как можно проще, часто так, чтобы он служил только одной цели, это увеличивает шансы на многократное использование кода.
В простом приложении счетчика, созданном выше, есть возможность абстрагировать некоторые элементы от компонента App
. Кнопки могут быть абстрагированы в собственный компонент, где мы передаем метод и метку кнопки в качестве пропсов.
Заголовок и название приложения также могут быть размещены в собственных компонентах. После того как мы абстрагировали все элементы, компонент App
может выглядеть примерно так:
import React, { useState } from "react";
import { Button } from "./Button";
import { Display } from "./Display";
import { Header } from "./Header";
import "./styles.css";
export default function App() {
const addOne = () => {
setCounter(counter + 1)
}
const subtractOne = () => {
setCounter(counter - 1);
}
const reset = () => {
setCounter(0);
}
const initialState = [
{operation: subtractOne, buttonLabel:"-"},
{operation: reset, buttonLabel: "reset"},
{operation: addOne, buttonLabel: "+"}
]
const [ counter, setCounter ] = useState(0);
const [ buttonContents, ] = useState(initialState)
return (
<div className="App">
<Header header="Simple React Counter"/>
<Display counter={counter}/>
{buttonContents.map(button => {
return (
<Button key={button.operation + button.buttonLabel} operation={button.operation} buttonLabel={button.buttonLabel} />
)
})}
</div>
);
}
Основная цель абстракции - сделать дочерние компоненты как можно более общими, чтобы их можно было использовать повторно в любом нужном вам виде. App-компонент должен содержать только ту информацию, которая специфична для приложения, и выводить или возвращать только более мелкие компоненты.
5. Соглашения об именовании
В React есть три основных соглашения об именовании, которые следует рассматривать как лучшие практики.
- Компоненты должны быть написаны в PascalCase - а также и названы по их основной функциональности, а не по специфике приложения (на случай, если вы измените ее позже).
- Элементы, которым нужны ключи, должны быть уникальными, неслучайными идентификаторами (например, отдельные карты или записи в карточной колоде или списке). Лучшая практика - не использовать для ключей только индексы. Допустимо назначение ключа, состоящего из конкатенации двух различных свойств объекта.
Основная цель ключа - хранить базовую информацию, чтобы React мог получить представление о том, что изменилось в приложении.
key={button.operation + button.buttonLabel}
- Методы должны быть в camelCase и называться по их назначению, а не быть специфичными для приложения. По тем же причинам, что и компоненты в PascalCase, методы должны быть названы по их назначению, а не по их особенности в приложении.
Итоги
В этой статье мы рассмотрели пять лучших практик, которые следует применять при разработке на React. Во многих случаях речь идет о том, что подходит вам и вашей команде, а не о том, что конкретно считается "лучшей практикой".
Именно разработчики, которые впоследствии будут редактировать и читать ваш код, должны будут уметь расшифровывать и улучшать ваш проект. Очередь обновлений состояния в React
Содержание туториала по React
Установка переменной состояния поставит в очередь следующий рендеринг. Но иногда нужно выполнить несколько операций над значением перед постановкой в очередь следующего рендеринга. Для этого полезно понять, как React пакетно обновляет состояние (батчинг).
React батчит обновления состояний
Батчинг - это объединение обновлений в одну операцию.
В следующем примере, вы можете ожидать, что нажатие кнопки «+3» увеличит счетчик три раза, потому что он трижды вызывает setNumber(number + 1)
:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}
>
+3
</button>
</>
);
}
Однако, как вы, возможно, помните из предыдущего раздела, значения состояния каждого рендеринга фиксированы, поэтому значение number
внутри обработчика событий первого рендеринга всегда равно 0
, независимо от того, сколько раз вы вызываете setNumber(1)
:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
Но здесь есть еще один фактор, который нужно обсудить. React ждет, пока весь код в обработчиках событий будет запущен, прежде чем обрабатывать ваши обновления состояния. Вот почему повторный рендеринг происходит только после всех этих вызовов setNumber()
.
Это может напомнить вам официанта, принимающего заказ в ресторане. Официант не бежит на кухню при упоминании вашего первого блюда. Вместо этого они позволяют вам закончить свой заказ, позволяют вносить в него изменения и даже принимать заказы от других людей за столом.
Это позволяет вам обновлять несколько переменных состояния — даже из нескольких компонентов — без запуска слишком большого количества повторных рендерингов. Но это также означает, что пользовательский интерфейс не будет обновляться до тех пор, пока ваш обработчик событий и любой код в нем не завершится. Такое поведение, также известное как пакетная обработка (батчинг), позволяет вашему приложению React работать намного быстрее. Это также позволяет избежать сбивающих с толку «недоделанных» рендеров, в которых были обновлены только некоторые переменные.
React не объединяет несколько преднамеренных событий, таких как клики, — каждый клик обрабатывается отдельно. Будьте уверены, что React выполняет пакетную обработку только тогда, когда это в целом безопасно. Это гарантирует, что, например, если первый клик кнопки отключит форму, второй щелчок не отправит ее снова.
Обновление одной и той же переменной состояния несколько раз перед следующим рендерингом
Это необычный вариант использования, но если вы хотите обновить одну и ту же переменную состояния несколько раз перед следующим рендерингом, вместо передачи следующего значения состояния, такого как setNumber(number + 1)
, вы можете передать функцию, которая вычисляет следующее состояние. на основе предыдущего в очереди, например setNumber(n => n + 1)
. Это способ сказать React «сделать что-нибудь со значением состояния», а не просто заменить его.
Вот как это выглядит на практике:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);
}}
>
+3
</button>
</>
);
}
Здесь n => n + 1
называется функцией обновления. Когда вы передаете его установщику состояния:
- React ставит эту функцию в очередь для обработки после выполнения всего остального кода в обработчике событий.
- Во время следующего рендеринга React проходит через очередь и выдает вам окончательное обновленное состояние.
setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);
Вот как React работает с этими строками кода при выполнении обработчика событий:
setNumber(n => n + 1)
: n => n + 1
— это функция. React добавляет его в очередь.
setNumber(n => n + 1)
: n => n + 1
— это функция. React добавляет его в очередь.
setNumber(n => n + 1)
: n => n + 1
— это функция. React добавляет его в очередь.
Когда вы вызываете useState
во время следующего рендеринга, React проходит через очередь. Предыдущее числовое состояние было 0
, так что это то, что React передает первой функции обновления в качестве аргумента n
. Затем React берет возвращаемое значение вашей предыдущей функции обновления и передает его следующему обновлению как n
и так далее:
запланированное обновление | n | возвращает |
---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React сохраняет 3
в качестве конечного результата и возвращает его из useState
.
Вот почему нажатие «+3» в приведенном выше примере корректно увеличивает значение на 3
.
Что произойдет, если вы обновите состояние после его замены
А как насчет этого обработчика событий? Как вы думаете, какое число будет в следующем рендере?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
}}
>
Increase the number
</button>
</>
);
}
Вот что этот обработчик событий говорит React:
setNumber(number + 5)
: number
равно 0
, поэтому получаем setNumber(0 + 5)
. React добавляет в свою очередь «заменить на 5».
setNumber(n => n + 1)
: n => n + 1
— это функция обновления. React добавляет эту функцию в свою очередь.
Во время следующего рендера React проходит через очередь состояний:
запланированное обновление | n | возвращает |
---|
"заменить на 5" | 0 (не используется) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React сохраняет 6
в качестве конечного результата и возвращает его из useState
.
Вы могли заметить, что setState(x)
на самом деле работает как setState(n => x)
, но n
не используется.
Что произойдет, если вы замените состояние после его обновления
Давайте попробуем еще один пример. Как вы думаете, какое число будет в следующем рендере?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
setNumber(42);
}}
>
Increase the number
</button>
</>
);
}
Вот как React работает с этими строками кода при выполнении этого обработчика событий:
setNumber(number + 5)
: number
равно 0
, поэтому setNumber(0 + 5)
. React добавляет в свою очередь «заменить на 5».
setNumber(n => n + 1)
: n => n + 1
— это функция обновления. React добавляет эту функцию в свою очередь.
setNumber(42)
: React добавляет в свою очередь «заменить на 42».
Во время следующего рендера React проходит через очередь состояний:
запланированное обновление | n | возвращает |
---|
"заменить на 5" | 0 (не используется) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
"заменить на 42" | 6 (не используется) | 42 |
Затем React сохраняет 42
как окончательный результат и возвращает его из useState
.
Подводя итог, вот как вы можете думать о том, что вы передаете установщику состояния setNumber
:
- Функция обновления (например,
n => n + 1
) добавляется в очередь.
- Любое другое значение (например, число
5
) добавляет в очередь «заменить на 5», игнорируя то, что уже находится в очереди.
После завершения обработчика событий React запустит повторный рендеринг. Во время повторного рендеринга React обработает очередь. Функции обновления запускаются во время рендеринга, поэтому функции обновления должны быть чистыми и возвращать только результат. Не пытайтесь установить состояние внутри них или запустить другие побочные эффекты. В строгом режиме React дважды запускает каждую функцию обновления (но отбрасывает второй результат), чтобы помочь вам найти ошибки.
Соглашения об именах
Аргумент функции обновления принято называть первыми буквами соответствующей переменной состояния:
setEnabled((e) => !e);
setLastName((ln) => ln.reverse());
setFriendCount((fc) => fc * 2);
Если вы предпочитаете более подробный код, другим распространенным соглашением является повторение полного имени переменной состояния, например setEnabled(enabled => !enabled)
, или использование префикса, такого как setEnabled(prevEnabled => !prevEnabled)
.
Резюме
- Установка состояния не изменяет переменную в существующем рендеринге, но запрашивает новый рендеринг.
- React обрабатывает обновления состояния после завершения работы обработчиков событий. Это называется батчингом.
- Чтобы обновить некоторое состояние несколько раз в одном событии, вы можете использовать функцию обновления
setNumber(n => n + 1)
.