Передача стейта в дочерний компонент в React
В прошлой статье «Дочерний компонент в React» было показано как вызывать компонент из компонента. В этой статье доработаем наш код небольшого интенет магазина. Добавим в него кнопку добавления товара в корзину. А чтобы состояние товара запоминалось, введём в начальный массив логическое свойство «inCart», а сам массив будем записывать в стейт, чтобы срабатывали механизмы реактивности.
Сначала модифицируем стартовый массив:
const shopItems = [ , , , ];
Теперь изменим компонент с выводом списка продуктов(«Products»), добавив в него стейт:
function Products() < const [prods, setProds] = useState(shopItems); const result = prods.map(prod => < return name= price= inCart= />; >); return ; >
И чтобы вся система с добавлением товаров в корзину работала, необходимо добавить кнопку для этого действия в карточку товара (в компонент «Product»):
function Product(< id, name, price, inCart >) < return Товар: , Цена: , , ; >
Мы хотим, чтобы при нажатии на кнопку «Добавить в корзину» у товара менялось свойство «inCart» на значение «true»:
function addToCart(id) < setProds(prods.map(prod => < if (prod.id == id) < prod.inCart = true; >return prod; >)); >
По правилам React сам компонент не должен изменять свои принимаемые аргументы (свойства). То есть на уровне компонента «Product» декларировать эту функцию. Поэтому придётся её переместить в родительский компонент. А в дочерний компонент будем передавать указатель на эту функцию. То есть в компоненте «Products» получится так:
const result = prods.map(prod => < return name= price= inCart= addToCart= />; >);
Остаётся только добавить событие на кнопку добавления товара в корзину компонента «Product». По клику будем вызывать функцию добавления и передавать ей id нужного товара
function Product(< id, name, price, inCart, addToCart >) < return Товар: , Цена: , , ; >
Так в дочернем компоненте будет вызываться функция, которая декларирована в родителе. React распознает изменения и перерендерит страницу, отобразив успешное добавление товара в корзину.
Передача данных между компонентами в React
В реакте поток данных — однонаправленный. Это значит что данные передаются как водопад, сверху вниз, от родителя к ребенку, через props. Что такое props? Это неизменяемый объект, предназначенный только для чтения. Проще понять если думать о компонентах, как о функциях(а они по сути ими и являются), props—это просто аргумент функции, с которым мы можем работать внутри, но не изменять.
Родитель ➡️ Ребенок — используй Props
Это самый простой способ передачи данных.
1. В родительском компоненте передаём данные из стейта в дочерний.
2. В дочернем принимаем и выводим.
Props:
Родитель ⬅️ Ребенок — используй Callback
Эта передача данных напоминает мне бумеранг, есть точка старта — это наш родительский компонент, есть точка максимального отдаления — это дочерний компонент. И есть наш инструмент —бумеранг, в реакте это будет функция, которая находится в родителе и передаётся через props в ребенка, где и вызывается.
1. Создаём в родителе функцию updateData . У неё есть входной параметр value , этот параметр мы присваиваем в стейт нашего компонента, с помощью функции setState . Бумеранг готов!
updateData = (value) => this.setState(< name: value >)>
2. Передаём в дочерний элемент через props функцию updateData . Мы запустили бумеранг 🙂
3. В дочернем элементе я создала кнопку, при клике на неё будет вызываться функция, которую мы передавали с помощью props. Ей передается новый параметр для текста, именно его мы хотим передать в родителя. Бумеранг возвращается назад.
Ребенок ➡️ Ребенок—используй родителя.
Для того чтобы передать данные между соседними компонентами, нужно использовать посредника—их родителя. Сначала нужно передать данные от ребенка к родителю как аргумент коллбека. Потом присвоить эти данные в стейт родителя и передать через props другому компоненту.
Как передавать данные между компонентами в ReactJS
В React можно разными способами передавать данные между компонентами. Применимость каждого способа определяется направлением движения данных. Данные могут двигаться от дочернего компонента к родительскому или наоборот. Данные могут двигаться глубоко — от корневого элемента до элемента-потомка. Данными могут обмениваться соседние элементы. В этой статье мы рассмотрим как передавать данные в каждом конкретном случае.
- от родительского компонента к дочернему;
- от дочернего компонента к родительскому;
- между соседними компонентами;
- от компонента к компоненту-потомку (через несколько уровней вниз);
- от компонента к компоненту-предку (через несколько уровней вверх).
- через пропсы;
- используя callback-функцию;
- пробросом пропсов от уровня к уровню (prop drilling);
- при помощи контекста (React Context AP);
- через хранилище (store);
От родительского компонента к дочернему
Наиболее простой и часто встречающийся случай — это случай, когда дочерний компонент принимает данные от родителя через пропсы.
import < useState >from 'react' const Parent = () => < const [value, setValue] = useState('') const handleChange = (event) => < setValue(event.target.value) >return ( />* передаем проп в дочерний компонент */> /> ) > const Child = (< value >) => < return ( Value is: '> ) >
От дочернего компонента к родительскому
Если необходимо передать данные от дочернего реакт компонента к родительскому, используются функции обратного вызова (callback-функции).
import < useState >from 'react' const Child = (< onChange >) => < const handleChange = (event) => < onChange(event.target.value) // callback-функция >return ( /> ) > const Parent = () => < const [value, setValue] = useState('') const handleChange = (value) => < setValue(value) >return ( Value is: '> /> ) >
Между соседними компонентами
Данные между соседними компонентами, т.е. между компонентами на одном уровне, можно передать через общий предок. Обычно данные от одного Реакт компонента передаются вверх, в компонент-предок, через callback-функцию, а компонент-предок передает их в другой компонент через проп.
import < useState >from 'react' const Parent = () => < const [value, setValue] = useState('') const handleChange = (value) => < setValue(value) >return ( /> /> ) > const Sibling1 = (< onChange >) => < const handleChange = (event) => < onChange(event.target.value) >return ( /> ) > const Sibling2 = (< value>) => < return ( Value is: '> ) >
Через несколько уровней вверх/вниз
Если компоненты находится в несколько уровнях друг от друга, то также можно передать проп. Этот проп придется описывать во всех компонентах на всех промежуточных уровнях. Эта ситуация называется prop drilling. Если уровней много, то такой способ покажется не очень удобным. Если нужно передавать данные на несколько уровней вверх, то также придется описывать и вызывать callback-функцию на всех промежуточных уровнях. Однако, в подобных случаях можно использовать Context API (пример которого приведен ниже), или state management библиотеки, такие как Redux, MobX, Recoil и т.д.
import < useState, useContext, createContext >from 'react' // создаем контекст const ValueContext = createContext() // Component1 записывает данные в контекст ValueContext const Component1 = () => < const < setValue >= useContext(ValueContext) const handleChange = (event) => < setValue(event.target.value) >return ( /> ) > // Component2 читает данные из контекста ValueContext const Component2 = () => < const < value >= useContext(ValueContext) return ( Value is: '> ) > // компоненты, которым необходим доступ к контексту, // должны быть обернуты в Provider export default function App() < const [value, setValue] = useState('') return ( >> ) >
Архитектура современного React приложения
8 месяцев назад · 6 мин. на чтение
Есть одна проблема, с которой сталкивается каждый React-разработчик на своем пути. Это то, как построить хорошую архитектуру приложения, какая структура приложения будет идеальной.
Эта статья поможет вам избежать некоторых распространенных ошибок, которые большинство разработчиков допускают при создании архитектуры приложений на реакте, и подскажет вам правильный способ структурирования директорий. Прежде чем начать, необходимо подчеркнуть один момент: не существует идеального решения, которое подходит для любого возможного случая. Это особенно важно понимать, потому что многие разработчики всегда ищут единственное и неповторимое решение всех своих проблем. Если вы попали сюда, это значит, что вы заинтересовались этой темой, так что пора начинать! Все содержимое, которое будет упоминаться, будет помещено в каталог src , и каждая упомянутая новая папка будет располагаться относительно этой директории.
Компоненты
Что в первую очередь создает React-разработчик в проекте? React-приложения создаются с помощью компонентов. Существует много различных архитектур (некоторые очень хорошие, а другие ужасные) и есть весьма универсальный путь, который можно использовать в большинстве случаев, даже для небольших проектов. Вот как это выглядит:
├── components │ ├── common │ │ └── button │ │ ├── button.tsx │ │ ├── button.stories.tsx │ │ ├── button.spec.tsx │ │ └── index.ts │ └── signup-form │ ├── signup-form.tsx │ ├── signup-form.spec.tsx │ └── index.ts
Ключевым моментом здесь является следующее: у нас есть папка components , содержащая все компоненты, которые используются в приложении более одного раза, поэтому мы собираемся исключить все специфические компоненты из этой папки. Почему? Просто потому, что смысл этой папки заключается в том, чтобы содержать логику многократного использования. Кнопка должна использоваться почти на каждой странице нашего приложения, поэтому и существует общая папка common . Для компонента signup-form происходит нечто иное. Почему? Предположим, что у нас есть две разные страницы (подробнее об этом позже) для входа и регистрации, компонент signup-form должен повторяться два раза, вот почему он помещен в папку components .
Обратите внимание, что это исключительный случай, если бы у нас была одна страница для аутентификации, нам не следовало бы помещать его в components . Вы, наверное, также заметили, что каждый компонент помещен в соответствующую директорию с очень простым для понимания соглашением об именовании.
button ├── button.tsx ├── button.stories.tsx ├── button.spec.tsx └── index.ts
- Все файлы, связанные с этим компонентом, находятся в этой папке.
- Все экспортируемые модули помещаются в index.ts , чтобы избежать двойного имени в импорте.
- Все файлы названы в kebab-case.
- Где находится компонент кнопки? -> В папке button .
- Где находятся сторибуки для этой кнопки? -> В папке button .
- Мне нужно найти тесты для этой кнопки, где я могу его найти? -> Очевидно, в папке button .
Страницы
Отдельной сущности для страниц в React не существует. Они тоже являются компонентами, состоящими из других компонентов. Но в отличие от других компонентов, обычно они очень строго привязаны (например, в определенный путь URL). Куда же их вставлять? Мы можем использовать каталог views (или pages , если хотите), в который помещаются все эти вещи, например:
views ├── home.tsx ├── guestbook.tsx └── newsletter ├── index.ts ├── newsletter.tsx └── components └── newsletter-form ├── newsletter-form.tsx ├── newsletter-form.spec.tsx └── index.ts
Для home и guestbook все довольно просто, страница должна быть результатом композиции других компонентов, которые имеют соответствующие тесты, поэтому для них нет специального каталога.
Иначе обстоит дело со страницей newsletter , на которой есть нечто специфическое, компонент newsletter-form . В этом случае используется подход создания вложенной папки компонентов внутри папки страницы и действуем так же, как в обычной папке компонентов, используя те же правила. Этот подход очень эффективен, поскольку позволяет разделить код на небольшие фрагменты, но при этом сохраняет хорошо организованную архитектуру. Компонент newsletter-form не следует помещать в папку с общими components , просто потому, что это единственное место, в котором он используется. Если приложение будет расти, и компонент будет использоваться в нескольких частях, ничто не помешает вам переместить его. Еще один совет - сохранять согласованное имя между страницей и маршрутом, примерно так:
Лэйауты (Layouts, Макеты)
Лэйауты вообще не являются страницами, они больше похожи на компоненты, поэтому с ними можно обращаться так же, но лучше помещать их в папку layouts , так понятнее, что в этом приложении есть n лэйаутов.
layouts ├── main.tsx └── auth.tsx
Вы можете заметить, что мы не называем их main-layout.tsx , а просто main.tsx , потому что, следуя этому шаблону, нам пришлось бы переименовать все компоненты, например, table-component.tsx , что странно. Поэтому называем все компоненты без очевидного суффикса, задаваемого родительским каталогом, а если нужно подчеркнуть, что используется макет, всегда можно использовать псевдоним импорта.
import < Main as MainLayout >from "@/layouts/main.tsx";
Контексты, хуки и хранилища
Это довольно просто, и обычно, почти все разработчики придерживаются чего-то подобного:
hooks ├── use-users.ts └── use-click-outside.ts contexts ├── workbench.tsx └── authentication.tsx
Опять же, для единообразия используется kebab-case для всех имен файлов, так что нам нужно беспокоиться о том, какие из них написаны заглавными буквами, а какие нет. Для тестовых файлов, из-за того, что пользовательских хуков немного, не обязательно создавать отдельную папку, но, если вы хотите быть очень строгими, вы можете сделать и это:
hooks ├── use-users │ ├── use-users.ts │ ├── use-users.spec.ts │ └── index.ts └── use-click-outside.ts
Функции-помощники (хэлперы, helpers)
Сколько раз вы создавали красивую функцию formatCurrency , не зная, куда ее положить? Папка helpers придет вам на помощь. Обычно сюда помещаются все файлы, которые используются для того, чтобы код выглядел лучше. Не важно, будет ли функция использоваться несколько раз или нет.
helpers ├── format-currency.ts ├── uc-first.ts └── pluck.ts
Константы
Существует много проектов, которые содержат константы в папке utils или helpers , но лучше помещать их в отдельный файл, давая пользователю хороший обзор того, что используется в качестве константы в приложении. Чаще всего в эту папку помещаются только глобальные константы, поэтому не стоит помещать сюда константу QUERY_LIMIT , если она используется только в одной функции для очень специфического случая.
constants └── index.ts
Кроме того, можно хранить все константы в одном файле. Нет смысла разбивать каждую константу на отдельные файлы.
// @/constants/index.ts export const COMPLANY_EMAIL = "example@example.com";
И использоваться они будут так:
import < COMPLANY_EMAIL >from "@/constants";
Стили
Просто поместите глобальные стили в папку styles , и все готово.
styles ├── index.css ├── colors.css └── typography.css
А как насчет CSS для компонентов? Хороший вопрос. Помните папку компонентов, о которой мы говорили некоторое время назад? Так вот, вы можете добавить больше файлов в зависимости от ваших потребностей.
button ├── button.tsx ├── button.stories.tsx ├── button.styled.tsx ├── button.module.scss ├── button.spec.tsx └── index.ts
Если вы используете emotion , styled-components или просто CSS Modules, поместите их в папку конкретного компонента, чтобы все было оптимально упаковано.
Конфигурационные файлы
Есть ли у вашего приложения файлы конфигурации, такие как Dockerfiles, Fargate Task Definitions, webpack и т.д.? Папка config должна быть идеальным местом для них. Помещение их в соответствующую директорию позволяет избежать загрязнения корневого каталога не относящимися к делу файлами.
API
99% приложений react имеют хотя бы один вызов API к внешнему источнику данных (вашему бэкенду или какому-то публичному сервису), обычно эти операции выполняются в нескольких строках кода без особых сложностей, и именно поэтому, оптимальная их организация игнорируется. Рассмотрим этот код:
axios .get("https://api.service.com/bookings") .then((res) => setBookings(res.data)) .catch((err) => setError(err.message));
Довольно просто, верно? Теперь представьте, что у вас есть эти 3 строки, распределенные по 10 компонентам, потому что вы часто используете именно этот адрес сервера. Надеюсь, вы не хотите выполнять поиск и замену всех URL в приложении, кроме того, если вы используете TypeScript, импортировать каждый раз тип ответа - это довольно повторяющееся действие.
Вместо этого рассмотрите возможность использования каталога api , который, прежде всего, содержит экземпляр клиента, используемого для вызовов, например, fetch или axios , а также файлы, содержащие декларации вызовов fetch .
api ├── client.ts ├── users.ts └── bookings.ts
И пример файла users.ts:
export type User = < id: string; firstName: string; lastName: string; email: string; >; export const fetchUsers = () => < return client.get("/users", < baseURL: "https://api.service.com/v3/", >); >;
Итоги
Это был долгий путь, и надеюсь, что информация в этой статье окажется полезной для вас при создании новых и существующих проектов. Еще многое предстоит сказать, всегда есть особые случаи, которые нужно принимать во внимание, но пункты, рассмотренные в этой статье, являются наиболее используемыми многими react разработчиками.
Подъём состояния
Часто несколько компонентов должны отражать одни и те же изменяющиеся данные. Мы рекомендуем поднимать общее состояние до ближайшего общего предка. Давайте посмотрим, как это работает.
В этом разделе мы создадим калькулятор температуры, вычисляющий вскипит ли вода при заданной температуре.
Мы начнём с компонента под названием BoilingVerdict . Он принимает температуру по шкале Цельсия в качестве пропа celsius и выводит, достаточна ли температура для кипения воды:
function BoilingVerdict(props) if (props.celsius >= 100) return p>Вода закипит.p>; > return p>Вода не закипит.p>;>
Затем мы создадим компонент Calculator . Он рендерит для ввода температуры и сохраняет её значение в this.state.temperature .
Кроме того, он рендерит BoilingVerdict для текущего значения поля ввода.
class Calculator extends React.Component constructor(props) super(props); this.handleChange = this.handleChange.bind(this); this.state = temperature: ''>; > handleChange(e) this.setState(temperature: e.target.value>); > render() const temperature = this.state.temperature; return ( fieldset> legend>Введите температуру в градусах Цельсия:legend> input value=temperature> onChange=this.handleChange> /> BoilingVerdict celsius=parseFloat(temperature)> /> fieldset> ); > >
Добавление второго поля ввода
Добавим к полю ввода градусов Цельсия поле ввода по шкале Фаренгейта. Оба поля будут синхронизироваться.
Мы можем начать с извлечения компонента TemperatureInput из Calculator . Добавим в него новый проп scale , значением которого может быть либо "c" или "f" :
const scaleNames = c: 'Цельсия', f: 'Фаренгейта'>; class TemperatureInput extends React.Component constructor(props) super(props); this.handleChange = this.handleChange.bind(this); this.state = temperature: ''>; > handleChange(e) this.setState(temperature: e.target.value>); > render() const temperature = this.state.temperature; const scale = this.props.scale; return ( fieldset> legend>Введите температуру в градусах scaleNames[scale]>:legend> input value=temperature> onChange=this.handleChange> /> fieldset> ); > >
Теперь можем изменить Calculator для рендера двух отдельных полей ввода температуры:
class Calculator extends React.Component render() return ( div> TemperatureInput scale="c" /> TemperatureInput scale="f" /> div> ); > >
Сейчас у нас есть два поля ввода, но когда вы вводите температуру в одно из них, другое поле не обновляется. Это противоречит нашему требованию — мы хотим их синхронизировать.
Мы также не можем отображать BoilingVerdict из Calculator . Компонент Calculator не знает текущую температуру, потому что она находится внутри TemperatureInput .
Написание функций для конвертации температур
Во-первых, мы напишем две функции для конвертации градусов по шкале Цельсия в Фаренгейт и обратно:
function toCelsius(fahrenheit) return (fahrenheit - 32) * 5 / 9; > function toFahrenheit(celsius) return (celsius * 9 / 5) + 32; >
Эти две функции конвертируют числа. Мы напишем ещё одну функцию, которая принимает строку с температурой ( temperature ) и функцию конвертации ( convert ) в качестве аргументов, и возвращает строку. Мы будем использовать эту функцию для вычисления значения одного поля ввода на основе значения из другого поля ввода.
Данная функция возвращает пустую строку при некорректном значении аргумента temperature и округляет возвращаемое значение до трёх чисел после запятой:
function tryConvert(temperature, convert) const input = parseFloat(temperature); if (Number.isNaN(input)) return ''; > const output = convert(input); const rounded = Math.round(output * 1000) / 1000; return rounded.toString(); >
Например, вызов tryConvert('abc', toCelsius) возвратит пустую строку, а вызов tryConvert('10.22', toFahrenheit) — '50.396' .
В настоящее время оба компонента TemperatureInput независимо хранят свои значения каждое в собственном локальном состоянии:
class TemperatureInput extends React.Component constructor(props) super(props); this.handleChange = this.handleChange.bind(this); this.state = temperature: ''>; > handleChange(e) this.setState(temperature: e.target.value>); > render() const temperature = this.state.temperature; // .
Однако мы хотим, чтобы эти два поля ввода синхронизировались друг с другом. Когда мы обновляем поле ввода градусов по Цельсию, поле ввода градусов по Фаренгейту должно отражать сконвертированную температуру и наоборот.
В React совместное использование состояния достигается перемещением его до ближайшего предка компонентов, которым оно требуется. Это называется «подъём состояния». Мы удалим внутреннее состояние из TemperatureInput и переместим его в Calculator .
Если Calculator владеет общим состоянием, он становится «источником истины» текущей температуры для обоих полей ввода. Он может предоставить им значения, которые не противоречат друг другу. Поскольку пропсы обоих компонентов TemperatureInput приходят из одного и того же родительского компонента Calculator , два поля ввода будут всегда синхронизированы.
Давайте шаг за шагом посмотрим, как это работает.
Во-первых, мы заменим this.state.temperature на this.props.temperature в компоненте TemperatureInput . Пока давайте представим, что this.props.temperature уже существует, хотя нам нужно будет передать его из Calculator в будущем:
render() // Ранее было так: const temperature = this.state.temperature; const temperature = this.props.temperature; // .
Мы знаем, что пропсы доступны только для чтения. Когда temperature находилась во внутреннем состоянии, TemperatureInput мог просто вызвать this.setState() для изменения его значения. Однако теперь, когда temperature приходит из родительского компонента в качестве пропа, TemperatureInput не может контролировать его.
В React это обычно решается путём создания «управляемого» компонента. Точно так же, как DOM-элемент принимает атрибуты value и onChange , так и пользовательский TemperatureInput принимает оба пропса temperature и onTemperatureChange от своего родителя Calculator .
Теперь, когда TemperatureInput хочет обновить свою температуру, он вызывает this.props.onTemperatureChange :
handleChange(e) // Ранее было так: this.setState(); this.props.onTemperatureChange(e.target.value); // .
Примечание:
В пользовательских компонентах нет особого смысла в именах пропсов temperature или onTemperatureChange . Мы могли бы назвать их как-то иначе, например, value и onChange , т. к. подобные имена — распространённое соглашение.
Пропсы onTemperatureChange и temperature будут предоставлены родительским компонентом Calculator . Он будет обрабатывать изменения, модифицируя собственное внутреннее состояние, тем самым повторно отрендеривая оба поля ввода с новыми значениями. Вскоре мы рассмотрим новую реализацию Calculator .
Прежде чем изменить Calculator , давайте вспомним, что поменялось в компоненте TemperatureInput . Мы удалили из него внутреннее состояние, и вместо this.state.temperature теперь используем this.props.temperature . Вместо вызова this.setState() , когда мы хотим изменить состояние, теперь вызываем this.props.onTemperatureChange() , который получен от компонента Calculator :
class TemperatureInput extends React.Component constructor(props) super(props); this.handleChange = this.handleChange.bind(this); > handleChange(e) this.props.onTemperatureChange(e.target.value); > render() const temperature = this.props.temperature; const scale = this.props.scale; return ( fieldset> legend>Введите градусы по шкале scaleNames[scale]>:legend> input value=temperature> onChange=this.handleChange> /> fieldset> ); > >
Теперь перейдём к компоненту Calculator .
Мы будем хранить текущие значения temperature и scale во внутреннем состоянии этого компонента. Это состояние, которое мы «подняли» от полей ввода, и теперь оно будет служить «источником истины» для них обоих. Это минимальное представление всех данных, про которое нам нужно знать для рендера обоих полей ввода.
Например, если мы вводим 37 как значение поля ввода для температуры по шкале Цельсия, состояние компонента Calculator будет:
temperature: '37', scale: 'c' >
Если позднее мы изменим поле для ввода градусов по шкале Фаренгейта на 212, состояние Calculator будет:
temperature: '212', scale: 'f' >
Мы могли бы сохранить значения обоих полей ввода, но это оказалось бы ненужным. Достаточно сохранить значение последнего изменённого поля ввода и шкалу, которая это значение представляет. Затем мы можем вывести значение для другого поля ввода, основываясь только на текущих значениях temperature и scale .
Поля ввода остаются синхронизированными, поскольку их значения вычисляются из одного и того же состояния:
class Calculator extends React.Component constructor(props) super(props); this.handleCelsiusChange = this.handleCelsiusChange.bind(this); this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this); this.state = temperature: '', scale: 'c'>; > handleCelsiusChange(temperature) this.setState(scale: 'c', temperature>); > handleFahrenheitChange(temperature) this.setState(scale: 'f', temperature>); > render() const scale = this.state.scale; const temperature = this.state.temperature; const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature; const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature; return ( div> TemperatureInput scale="c" temperature=celsius> onTemperatureChange=this.handleCelsiusChange> /> TemperatureInput scale="f" temperature=fahrenheit> onTemperatureChange=this.handleFahrenheitChange> /> BoilingVerdict celsius=parseFloat(celsius)> /> div> ); > >
Теперь, независимо от того, какое поле ввода вы редактируете, this.state.temperature и this.state.scale в Calculator обновляются. Одно из полей ввода получает значение как есть, поэтому введённые пользователем данные сохраняются, а значение другого поля ввода всегда пересчитывается на их основе.
Давайте посмотрим, что происходит, когда вы редактируете поле ввода:
- React вызывает функцию, указанную в onChange на DOM-элементе . В нашем случае это метод handleChange() компонента TemperatureInput .
- Метод handleChange() в компоненте TemperatureInput вызывает this.props.onTemperatureChange() с новым требуемым значением. Его пропсы, включая onTemperatureChange , были предоставлены его родительским компонентом — Calculator .
- Когда Calculator рендерился ранее, он указал, что onTemperatureChange в компоненте TemperatureInput по шкале Цельсия — это метод handleCelsiusChange в компоненте Calculator , а onTemperatureChange компонента TemperatureInput по шкале Фаренгейта — это метод handleFahrenheitChange в компоненте Calculator . Поэтому один из этих двух методов Calculator вызывается в зависимости от того, какое поле ввода редактируется.
- Внутри этих методов компонент Calculator указывает React сделать повторный рендер себя, используя вызов this.setState() со значением нового поля ввода и текущей шкалой.
- React вызывает метод render() компонента Calculator , чтобы узнать, как должен выглядеть UI. Значения обоих полей ввода пересчитываются исходя из текущей температуры и шкалы. В этом методе выполняется конвертация температуры.
- React вызывает методы render() конкретных компонентов TemperatureInput с их новыми пропсами, переданными компонентом Calculator . Он узнает, как должен выглядеть UI.
- React вызывает метод render() компонента Boiling Verdict , передавая температуру в градусах Цельсия как проп.
- React DOM обновляет DOM, чтобы привести его в соответствие с нужными нам значениями в полях ввода. Отредактированное нами только что поле ввода получает его текущее значение, а другое поле ввода обновляется конвертированным значением температуры.
Каждое обновление проходит через одни и те же шаги, поэтому поля ввода остаются синхронизированными.
Для любых изменяемых данных в React-приложении должен быть один «источник истины». Обычно состояние сначала добавляется к компоненту, которому оно требуется для рендера. Затем, если другие компоненты также нуждаются в нём, вы можете поднять его до ближайшего общего предка. Вместо того, чтобы пытаться синхронизировать состояние между различными компонентами, вы должны полагаться на однонаправленный поток данных.
Для подъёма состояния приходится писать больше «шаблонного» кода, чем при подходах с двусторонней привязкой данных, но мы получаем преимущество в виде меньших затрат на поиск и изолирование багов. Так как любое состояние «живёт» в каком-нибудь компоненте, и только этот компонент может его изменить, количество мест с возможными багами значительно уменьшается. Кроме того, вы можете реализовать любую пользовательскую логику для отклонения или преобразования данных, введённых пользователем.
Если что-то может быть вычислено из пропсов или из состояния, то скорее всего оно не должно находиться в состоянии. Например, вместо сохранения celsiusValue и fahrenheitValue , мы сохраняем только последнюю введённую температуру ( temperature ) и её шкалу ( scale ). Значение другого поля ввода можно всегда вычислить из них в методе render() . Это позволяет очистить или применить округление к значению другого поля, не теряя при этом точности значений, введённых пользователем.
Когда вы видите, что в UI что-то отображается неправильно, то можете воспользоваться расширением React Developer Tools. С помощью него можно проверить пропсы и перемещаться по дереву компонентов вверх до тех пор, пока не найдёте тот компонент, который отвечает за обновление состояния. Это позволяет отследить источник багов: