Подробное руководство по итерации, контексту и дочерним элементам в React
Чтобы следовать этому руководству, вы можете создать ветку в репозитории CodeSandBox. Вы также можете посмотреть здесь: GitHub Gist. Давайте начнем!
Итерация наследуемых элементов React
Содержание статьи:
Часто мы работаем с большими массивами данных, которые затем необходимо отобразить в пользовательском интерфейсе. Мы можем значительно упростить эту задачу, используя циклы или итерации, однако разработчики часто путаются, решая, что итерировать.
В React мы можем добавлять выражения JSX в пользовательский интерфейс, мы также можем добавлять и массивы JSX, то есть, когда мы перебираем данные, мы надеемся получить массив в конце.
В нашем стартовом коде вы заметите, что у нас есть данные об исходных 150 покемонах в файле /src/data/data.js в качестве примера. Создайте новый файл с именем src/components/Pokemon.js со следующим шаблоном:
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
JavaScript function Pokemon(props) { return <div>Pokemon</div>; } export default Pokemon;
12345 | function Pokemon(props) { return <div>Pokemon</div>;} export default Pokemon; |
Теперь давайте импортируем компонент в наше приложение:
JavaScript import Pokemon from “./components/Pokemon”; import “./styles.css”; export default function App() { return ( <div className=”App”> <Pokemon /> </div> ); }
12345678910 | import Pokemon from “./components/Pokemon”;import “./styles.css”; export default function App() { return ( <div className=”App”> <Pokemon /> </div> );} |
Давайте посмотрим, как мы можем перебрать наш Pokemon, используя традиционный цикл for:
Создать пустой массив
Для каждого элемента в массиве мы будем добавлять выражение JSX в массив.
Мы будем отображать массив в возвращенном JSX компоненте.
Добавьте приведенный ниже код в Pokemon.js:
JavaScript 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;
123456789101112131415161718192021222324 | 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, например:
JavaScript 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;
12345678910111213141516171819202122 | 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, как более плавный подход к циклу по массиву:
JavaScript Array.map((item, index) => {})
1 | Array.map((item, index) => {}) |
Метод map принимает функцию, и каждый элемент массива передается этой функции. На каждой итерации создается новый массив возвращаемого значения. Если мы передаем в map, функцию, которая возвращает желаемый JSX, она вернет наш массив JSX, и нам не придется беспокоиться об объявлении массива или добавлении в него значений.
В нашем примере описанный выше процесс будет выглядеть следующим образом:
JavaScript 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;
1234567891011121314151617 | 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 следующим образом:
JavaScript 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;
123456789101112131415161718192021 | 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 со следующим кодом:
JavaScript 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
12345678910111213 | 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:
JavaScript 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;
1234567891011121314151617 | 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, который не требует явного объявления в следующем стиле:
JavaScript <Component prop1=”value1″ prop2=”value2″>This text is passed as the children prop</Component>
1 | <Component prop1=”value1″ prop2=”value2″>This text is passed as the children prop</Component> |
Вместо этого prop Children состоит из всего, что находится между открывающим и закрывающим тегами компонента. Создайте новый файл src/components/Children.js. Добавьте следующий шаблон в Children.js:
JavaScript function Children(props) { console.log(props.children); return <h1>Children</h1>; } export default Children;
12345678 | function Children(props) { console.log(props.children); return <h1>Children</h1>;} export default Children; |
Давайте используем наш компонент children.js в App.js:
JavaScript 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> ); }
1234567891011121314 | 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:
JavaScript <Children> {[1,2,3,4,5]} </Children>
123 | <Children> {[1,2,3,4,5]}</Children> |
Теперь children должен содержать массив:
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас! JavaScript <Children> {() => {console.log(“hello world”)}} </Children>
123 | <Children> {() => {console.log(“hello world”)}}</Children> |
И children теперь должен содержать функцию:
JavaScript <Children> Some text before the function {() => {console.log(“hello world”)}} </Children>
1234 | <Children> Some text before the function {() => {console.log(“hello world”)}}</Children> |
Children теперь должен быть массивом с текстом в качестве первого элемента и функцией в качестве второго. По сути, вы можете передавать любой тип данных в качестве children prop, и вы даже можете передавать несколько значений в качестве children prop.
Мы можем использовать это в своих интересах. Обновите код еще раз следующим образом:
JavaScript <Children> {(props) => <h1>{props.word}</h1>} </Children>
123 | <Children> {(props) => <h1>{props.word}</h1>} </Children> |
Обратите внимание, что функция выглядит как компонент, функция, которая получает props и возвращает JSX. Давайте рефакторим Children.js, чтобы воспользоваться этим:
JavaScript function Children(props) { console.log(props.children); return <div><props.children word=”it works”/></div>; } export default Children;
1234567 | 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, чтобы сделать имя каждого покемона полностью заглавным и розовым:
JavaScript 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;
12345678910111213141516171819202122 | import { createTransform } from “merced-react-hooks”; // Create Transform Component that takes child and returns replacementconst 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:
JavaScript 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)
123456789101112131415161718192021222324 | import { createContext, useContext } from “react”; // The Data to be Sharedconst theme = { backgroundColor: “navy”, color: “white”, border: “3px solid brown”}; // create a new context objectconst ThemeContext = createContext(theme); // custom provider wrapper componentexport const ThemeProvider = (props) => { // value prop determines what data is shared return ( <ThemeContext.Provider value={theme}> {props.children} </ThemeContext.Provider> );}; // custom hook to retreive dataexport const useTheme = () => useContext(ThemeContext) |
Теперь мы можем импортировать компонент-оболочку в Index.js:
JavaScript 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 );
123456789101112131415 | 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:
JavaScript 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;
12345678910111213141516171819202122232425 | import { createTransform } from “merced-react-hooks”;import { useTheme } from “../context/Theme”; // Create Transform Component that takes child and returns replacementconst 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:
JavaScript 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)
12345678910111213141516171819202122232425262728 | import { createContext, useContext, useState } from “react”; // create a new context objectconst CounterContext = createContext(null); // custom provider wrapper componentexport 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 dataexport const useCounter = () => useContext(CounterContext) |
index.js:
JavaScript 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 );
123456789101112131415161718 | 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 следующим образом:
JavaScript 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
1234567891011121314151617 | 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:
JavaScript 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> ); }
123456789101112131415 | 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
Источник: webformyself.com