От автора: самые сложные этапы освоения React включают итерацию по наборам информации, передачу данных по всему приложению и работу с props.children. В этой статье мы подробно рассмотрим эти три концепции, охватив их внутреннюю работу с несколькими соответствующими примерами.
Чтобы следовать этому руководству, вы можете создать ветку в репозитории CodeSandBox. Вы также можете посмотреть здесь: GitHub Gist. Давайте начнем!
Итерация наследуемых элементов React
Часто мы работаем с большими массивами данных, которые затем необходимо отобразить в пользовательском интерфейсе. Мы можем значительно упростить эту задачу, используя циклы или итерации, однако разработчики часто путаются, решая, что итерировать.
В React мы можем добавлять выражения JSX в пользовательский интерфейс, мы также можем добавлять и массивы JSX, то есть, когда мы перебираем данные, мы надеемся получить массив в конце.
В нашем стартовом коде вы заметите, что у нас есть данные об исходных 150 покемонах в файле /src/data/data.js в качестве примера. Создайте новый файл с именем src/components/Pokemon.js со следующим шаблоном:
function Pokemon(props) { return <div>Pokemon</div>; } export default Pokemon;
Теперь давайте импортируем компонент в наше приложение:
import Pokemon from "./components/Pokemon"; import "./styles.css"; export default function App() { return ( <div className="App"> <Pokemon /> </div> ); }
Давайте посмотрим, как мы можем перебрать наш Pokemon, используя традиционный цикл for:
Создать пустой массив
Для каждого элемента в массиве мы будем добавлять выражение JSX в массив.
Мы будем отображать массив в возвращенном JSX компоненте.
Добавьте приведенный ниже код в Pokemon.js:
import data from "../data/data"; function Pokemon(props) { //array to hold jsx for pokemon const pokemonJSX = []; //loop over data for (let index = 0; index < data.length; index++) { // variable with current pokemon const poke = data[index]; // push jsx into array pokemonJSX.push( <section key={poke.name}> <h1>{poke.name}</h1> <img src={poke.img} alt={poke.name} /> </section> ); } // render data in jsx return <div>{pokemonJSX}</div>; } export default Pokemon;
Обратите внимание на key prop в теге section. Всякий раз, когда мы создаем массив JSX, элемент верхнего уровня всегда должен иметь key prop с уникальным значением, помогая React повторно отображать эти массивы более эффективно.
Хотя этот код работает, он слишком большой. Мы потенциально могли бы очистить код с помощью цикла for of, например:
import data from "../data/data"; function Pokemon(props) { //array to hold jsx for pokemon const pokemonJSX = []; //loop over data for (let poke of data) { // push jsx into array pokemonJSX.push( <section key={poke.name}> <h1>{poke.name}</h1> <img src={poke.img} alt={poke.name} /> </section> ); } // render data in jsx return <div>{pokemonJSX}</div>; } export default Pokemon;
Мы по-прежнему получаем желаемый результат, однако логика пользовательского интерфейса немного «рассеивается», а не остается в возвращаемом значении компонента. Мы можем использовать метод массива, map, как более плавный подход к циклу по массиву:
Array.map((item, index) => {})
Метод map принимает функцию, и каждый элемент массива передается этой функции. На каждой итерации создается новый массив возвращаемого значения. Если мы передаем в map, функцию, которая возвращает желаемый JSX, она вернет наш массив JSX, и нам не придется беспокоиться об объявлении массива или добавлении в него значений.
В нашем примере описанный выше процесс будет выглядеть следующим образом:
import data from "../data/data"; function Pokemon(props) { // render data in jsx return ( <div> {data.map((poke) => ( <section key={poke.name}> <h1>{poke.name}</h1> <img src={poke.img} alt={poke.name} /> </section> ))} </div> ); } export default Pokemon;
Теперь вся логика представления наших компонентов находится в одном месте. Хотя иногда бывает сложно уложиться в map. Во фреймворке SolidJS, который также использует JSX, есть компонент built для зацикливания данных. Я создал аналогичный компонент в своей библиотеке React merced-react-hooks, который загружается в стартовый код.
Используя компонент loop, мы можем абстрагировать map следующим образом:
import data from "../data/data"; import { Loop } from "merced-react-hooks"; function Pokemon(props) { // render data in jsx return ( <div> <Loop withthis={data} dothat={(poke) => ( <section key={poke.name}> <h1>{poke.name}</h1> <img src={poke.img} alt={poke.name} /> </section> )} /> </div> ); } export default Pokemon;
Теперь мы видим огромную разницу в нашем коде, но дополнительная семантика может упростить его понимание.
Использование компонентов в итерации
Мы можем сделать наш код еще чище, создав компонент, отвечающий за отрисовку одного покемона. Создайте новый файл с именем src/components/OnePokemon.js со следующим кодом:
function OnePokemon(props) { const poke = props.poke; return ( <section key={poke.name}> <h1>{poke.name}</h1> <img src={poke.img} alt={poke.name} /> </section> ); } export default OnePokemon
Теперь мы можем очистить компонент Pokemon и сделать его еще более понятным. Добавьте приведенный ниже код в Pokemon.js:
import data from "../data/data"; import { Loop } from "merced-react-hooks"; import OnePokemon from "./OnePokemon"; function Pokemon(props) { // render data in jsx return ( <div> <Loop withthis={data} dothat={(poke) => <OnePokemon poke={poke} key={poke.name} />} /> </div> ); } export default Pokemon;
В настоящее время Pokemon.js обрабатывает только итерацию данных Pokemon. OnePokemon управляет тем, как появится один покемон. Написание нашего кода таким образом обеспечивает более четкое разделение задач, поэтому файлы компонентов легче читать.
Props.children
Children — это prop, который не требует явного объявления в следующем стиле:
<Component prop1="value1" prop2="value2">This text is passed as the children prop</Component>
Вместо этого prop Children состоит из всего, что находится между открывающим и закрывающим тегами компонента. Создайте новый файл src/components/Children.js. Добавьте следующий шаблон в Children.js:
function Children(props) { console.log(props.children); return <h1>Children</h1>; } export default Children;
Давайте используем наш компонент children.js в App.js:
import Pokemon from "./components/Pokemon"; import Children from "./components/Children"; import "./styles.css"; export default function App() { return ( <div className="App"> <Children> This text is the children prop </Children> <Pokemon /> </div> ); }
Код между открытым и закрывающим тегами передается как children prop и поэтому регистрируется. Это можно использовать достаточно гибко. Попробуйте следующий код, а затем просмотрите результаты console.log:
<Children> {[1,2,3,4,5]} </Children>
Теперь children должен содержать массив:
<Children> {() => {console.log("hello world")}} </Children>
И children теперь должен содержать функцию:
<Children> Some text before the function {() => {console.log("hello world")}} </Children>
Children теперь должен быть массивом с текстом в качестве первого элемента и функцией в качестве второго. По сути, вы можете передавать любой тип данных в качестве children prop, и вы даже можете передавать несколько значений в качестве children prop.
Мы можем использовать это в своих интересах. Обновите код еще раз следующим образом:
<Children> {(props) => <h1>{props.word}</h1>} </Children>
Обратите внимание, что функция выглядит как компонент, функция, которая получает props и возвращает JSX. Давайте рефакторим Children.js, чтобы воспользоваться этим:
function Children(props) { console.log(props.children); return <div><props.children word="it works"/></div>; } export default Children;
Мы смогли использовать children prop в качестве компонента, потому что это была функция, отвечающая правилам компонента.
Сhildren позволяет нам создавать довольно уникальные служебные компоненты. Я создал функцию createTransform, чтобы упростить процесс внутри библиотеки merced-react-hooks. createTransform создает компоненты, которые выполняют преобразование своих дочерних элементов, например настраивают формат даты или капитализацию.
Давайте используем createTransform, чтобы сделать имя каждого покемона полностью заглавным и розовым:
import { createTransform } from "merced-react-hooks"; // Create Transform Component that takes child and returns replacement const AllCapsPink = createTransform((c) => ( <span style={{ color: "pink" }}>{c.toUpperCase()}</span> )); function OnePokemon(props) { const poke = props.poke; return ( <section key={poke.name}> <h1> <AllCapsPink>{poke.name}</AllCapsPink> </h1> <img src={poke.img} alt={poke.name} /> </section> ); } export default OnePokemon;
Передача данных в ваше приложение
Мы можем передавать данные в качестве свойств в наше приложение, однако это может стать довольно утомительным, поскольку наше дерево компонентов становится все больше и больше. Если у вас есть данные, которые совместно используются тремя или более компонентами, вы можете использовать функцию React под названием Context, чтобы сделать ее доступной для всего приложения.
У нас может быть несколько контекстов для доставки разных типов данных. Давайте создадим контекст для доставки данных. По сути, мы будем следовать схеме ниже:
Создадим новый объект контекста
Создадим переменную или состояние, если данные могут измениться
Создадим пользовательский компонент, чтобы обернуть провайдера. Провайдер — это компонент, отвечающий за доступность контекста.
Создадим собственный хук, чтобы упростить использование контекста в нашем приложении.
Создайте src/context/Theme.js:
import { createContext, useContext } from "react"; // The Data to be Shared const theme = { backgroundColor: "navy", color: "white", border: "3px solid brown" }; // create a new context object const ThemeContext = createContext(theme); // custom provider wrapper component export const ThemeProvider = (props) => { // value prop determines what data is shared return ( <ThemeContext.Provider value={theme}> {props.children} </ThemeContext.Provider> ); }; // custom hook to retreive data export const useTheme = () => useContext(ThemeContext)
Теперь мы можем импортировать компонент-оболочку в Index.js:
import { StrictMode } from "react"; import ReactDOM from "react-dom"; import { ThemeProvider } from "./context/Theme"; import App from "./App"; const rootElement = document.getElementById("root"); ReactDOM.render( <ThemeProvider> <StrictMode> <App /> </StrictMode> </ThemeProvider>, rootElement );
Мы можем легко извлекать эти данные из любого места, где они нам нужны, с помощью пользовательского хука. Вернемся к OnePokemon.js:
import { createTransform } from "merced-react-hooks"; import { useTheme } from "../context/Theme"; // Create Transform Component that takes child and returns replacement const AllCapsPink = createTransform((c) => ( <span style={{ color: "pink" }}>{c.toUpperCase()}</span> )); function OnePokemon(props) { // bring in theme data from context const theme = useTheme(); const poke = props.poke; return ( <section style={theme} key={poke.name}> <h1> <AllCapsPink>{poke.name}</AllCapsPink> </h1> <img src={poke.img} alt={poke.name} /> </section> ); } export default OnePokemon;
Использование React Context для передачи состояния
Давайте создадим еще один контекст, который будет передавать состояние, связанное со счетчиком.
Создадим новый контекст
Создадим новый компонент-оболочку, который объявляет состояние и определяет функцию для изменения этого состояния желаемым образом.
Provider получает объект с состоянием и вспомогательной функцией
Пользовательский хук доставляет данные туда, где это необходимо
Добавьте приведенный ниже код в Counter.js:
import { createContext, useContext, useState } from "react"; // create a new context object const CounterContext = createContext(null); // custom provider wrapper component export const CounterProvider = (props) => { // state to share and functions to update state const [counter, setCounter] = useState(0) const addOne = () => setCounter(counter + 1) const minusOne = () => setCounter(counter - 1) //package state and functions to be shipped by provider const providedValue = {counter, addOne, minusOne} // value prop determines what data is shared return ( <CounterContext.Provider value={providedValue}> {props.children} </CounterContext.Provider> ); }; // custom hook to retreive data export const useCounter = () => useContext(CounterContext)
index.js:
import { StrictMode } from "react"; import ReactDOM from "react-dom"; import { ThemeProvider } from "./context/Theme"; import { CounterProvider } from "./context/Counter"; import App from "./App"; const rootElement = document.getElementById("root"); ReactDOM.render( <CounterProvider> <ThemeProvider> <StrictMode> <App /> </StrictMode> </ThemeProvider> </CounterProvider>, rootElement );
Теперь создайте src/components/Counter.js и используйте CounterContext следующим образом:
import { useCounter } from "../context/Counter"; function Counter(props) { // get counter and supporting functions const { counter, addOne, minusOne } = useCounter(); //return JSX return ( <div> <h1>{counter}</h1> <button onClick={addOne}>Add</button> <button onClick={minusOne}>Minus</button> </div> ); } export default Counter
Давайте используем этот новый компонент в App.js:
import Pokemon from "./components/Pokemon"; import Children from "./components/Children"; import Counter from "./components/Counter" import "./styles.css"; export default function App() { return ( <div className="App"> <Counter/> <Counter/> <Children>This text is the children prop</Children> <Pokemon /> </div> ); }
Обратите внимание, что оба счетчика обновляются, потому что у них нет никакого внутреннего состояния. Вместо этого каждый экземпляр компонента счетчика извлекается из одного и того же общего состояния, которое мы доставляем через контекст, что является мощным способом упростить управление состоянием в приложении.
Заключение
Эффективная итерация, использование Children prop и доставка данных в приложение с помощью React Context позволит вам легче использовать возможности React. В этой статье мы подробно рассмотрели эти три тематические области, попутно рассмотрев несколько примеров кода. Надеюсь, вам понравилась статья.
Автор: Alex Merced
Источник: blog.logrocket.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен