Я создам простое приложение для реагирования с двумя входами, где их значения будут полностью синхронизированы. В этой статье я объясню, как легко отправлять данные между одноуровневыми реагирующими компонентами, используя самые последние функции реагирования.
Настроить среду React
Чтобы запустить новое приложение в React, вам не нужно беспокоиться обо всех настройках и конфигурациях. Я буду использовать Create React App
среду, которая довольно проста и хорошо документирована. Всю документацию по этой инструментальной цепочке вы можете найти здесь.
Создать проект React
npx create-react-app sync_inputs
После этого вам нужно перейти в папку sync_inputs
и запустить приложение.
cd sync_inputs npm start
Примечание. У вас могут возникнуть проблемы с запуском этого локального сервера. Если у вас появляется сообщение об ошибке типа Attempting to bind to HOST environment variable: x86_64-apple-darwin13.4.0
, то эта статья может быть вам полезна. Когда я попытался запустить свой сервер, у меня возникла ошибка TypeError: fsevents is not a constructor.
Я нашел много советов, как с этим бороться здесь, но лично мне помогли удалить все пакеты узлов и снова запустить npm install
. После этого я запустил npm start
и все заработало. Также нет необходимости использовать npm. Вы можете использовать пряжу, если она вам больше нравится.
Если все установлено и ваш локальный сервер запущен, вам нужно перейти http://localhost:3000
, и вы должны увидеть логотип реакции на темном фоне. Что-то подобное:
Теперь остановите локальный сервер, нажав ctrl + c
в вашем терминале, и установите еще пару зависимостей.
Установите базовую библиотеку Material UI и их библиотеку иконок для создания приятного пользовательского интерфейса.
npm install @material-ui/core
npm install @material-ui/icons
Теперь ваш файл package.json должен выглядеть так:
{ "name": "sync_inputs", "version": "0.1.0", "private": true, "dependencies": { "@material-ui/core": "^4.5.0", "@material-ui/icons": "^4.4.3", "react": "^16.10.2", "react-dom": "^16.10.2", "react-scripts": "3.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
Итак, теперь у нас есть рабочая среда со всеми необходимыми библиотеками. Пришло время приступить к созданию нашего приложения.
Создание компонентов React
В этой части я настрою структуру нашего проекта и создам весь интерфейс.
Сначала создайте новую папку components
в ./src
каталоге нашего проекта. После этого в ./src/components
создайте два новых компонента (Input_one.js, Input_two.js). После всех этих шагов структура вашего проекта должна выглядеть так:
В следующих шагах я изменю наши компоненты, чтобы создать красивый интерфейс.
Измените ./src/components/Input_one.js
, чтобы создать входной.
import React from 'react'; import {makeStyles} from "@material-ui/core/styles/index"; //Material UI components import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; //Material UI Icons import IconButton from '@material-ui/core/IconButton'; import SearchIcon from '@material-ui/icons/Search'; const useStyles = makeStyles({ root: { padding: '2px 4px', display: 'flex', alignItems: 'center', width: '100%', }, input: { marginLeft: '8px', flex: 1, }, iconButton: { padding: 10, } }); export default function Input_one() { const classes = useStyles(); return( <React.Fragment> <Grid item xs={12} md={6}> <Paper className={classes.root}> <InputBase className={classes.input} placeholder="Input one" /> <IconButton className={classes.iconButton} aria-label="search"> <SearchIcon /> </IconButton> </Paper> </Grid> </React.Fragment> ) }
Измените ./src/components/Input_two.js
, чтобы создать второй вход.
import React from 'react'; import {makeStyles} from "@material-ui/core/styles/index"; //Material UI components import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; //Material UI Icons import IconButton from '@material-ui/core/IconButton'; import SearchIcon from '@material-ui/icons/Search'; const useStyles = makeStyles({ root: { padding: '2px 4px', display: 'flex', alignItems: 'center', width: '100%', }, input: { marginLeft: '8px', flex: 1, }, iconButton: { padding: 10, } }); export default function Input_two() { const classes = useStyles(); return( <React.Fragment> <Grid item xs={12} md={6}> <Paper className={classes.root}> <InputBase className={classes.input} placeholder="Input two" /> <IconButton className={classes.iconButton} aria-label="search"> <SearchIcon /> </IconButton> </Paper> </Grid> </React.Fragment> ) }
Примечание. Input_one и Input_two почти одинаковы. Теперь основное различие в export default function Input_one()
и export default function Input_two()
Теперь измените ./src/App.js
файл. В этом файле я создам Контейнер, где будет храниться все содержимое нашего приложения, добавлю заголовок и импортирую наши ‹Input_one /› и ‹Input_two /› компоненты.
import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; //Material UI components import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import CssBaseline from '@material-ui/core/CssBaseline'; import Toolbar from '@material-ui/core/Toolbar'; //Import our Inputs import Input_one from './components/Input_one'; import Input_two from './components/Input_two'; //Styles const useStyles = makeStyles({ toolbarTitle: { flex: 1, }, }); function App() { const classes = useStyles(); return ( <Container maxWidth="lg"> <CssBaseline /> {/*Title*/} <Toolbar> <Typography component="h2" variant="h5" color="inherit" align="center" noWrap className={classes.toolbarTitle} > Pass data between react sibling components </Typography> </Toolbar> {/*Inputs*/} <Grid container spacing={1}> <Input_one/> <Input_two/> </Grid> </Container> ); } export default App;
После всех этих шагов перейдите к http://localhost:3000
. Наш проект должен выглядеть так:
Создать функциональность для передачи данных между компонентами
Согласно документации React
Контекст в основном используется, когда некоторые данные должны быть доступны многим компонентам на разных уровнях вложенности.
В ./src/App.js
создать объект контекста
// Create context object export const AppContext = React.createContext();
Затем установите объект начального состояния, где мы храним нашу дату, которая должна быть доступна из обоих входов.
// Set up Initial State const initialState = { inputText: '', };
Теперь создайте функцию reducer
, которая будет обновлять наше начальное состояние каждый раз, когда значение любых входов будет изменено. Мы будем использовать эту функцию с useReducer
function reducer(state, action) { switch (action.type) { case 'UPDATE_INPUT': return { inputText: action.data }; default: return initialState; } }
Согласно реагирующей документации
useReducer
позволяет оптимизировать производительность компонентов, запускающих глубокие обновления, поскольку вы можете передаватьdispatch
вниз вместо обратных вызовов.
Сначала нам нужно импортировать useReducer
из react. Итак, теперь первая строка ./src/App.js
должна выглядеть так
import React, { useReducer } from 'react';
Теперь нам нужно инициализировать состояние useReducer
. Самый простой способ - передать начальное состояние в качестве второго аргумента. Мы можем сделать это, добавив:
const [state, dispatch] = useReducer(reducer, initialState);
И последняя часть в ./src/App.js
- обернуть компоненты ‹Input_one /› и ‹Input_two /› в AppContext.Provider.
. Каждый объект Context поставляется с компонентом Provider React, который позволяет потребляющим компонентам подписываться. к изменениям контекста. Провайдеры могут быть вложенными для переопределения значений глубже в дереве.
Теперь ./src/App.js
должен выглядеть так:
import React, { useReducer } from 'react'; import { makeStyles } from '@material-ui/core/styles'; //Material UI components import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import CssBaseline from '@material-ui/core/CssBaseline'; import Toolbar from '@material-ui/core/Toolbar'; //Import our Inputs import Input_one from './components/Input_one'; import Input_two from './components/Input_two'; //Styles const useStyles = makeStyles({ toolbarTitle: { flex: 1, }, }); // Create context object export const AppContext = React.createContext(); // Set up Initial State const initialState = { inputText: '', }; function reducer(state, action) { switch (action.type) { case 'UPDATE_INPUT': return { inputText: action.data }; default: return initialState; } } function App() { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); return ( <Container maxWidth="lg"> <CssBaseline /> {/*Title*/} <Toolbar> <Typography component="h2" variant="h5" color="inherit" align="center" noWrap className={classes.toolbarTitle} > Pass data between react sibling components </Typography> </Toolbar> {/*Inputs*/} <Grid container spacing={1}> <AppContext.Provider value={{ state, dispatch }}> <Input_one/> <Input_two/> </AppContext.Provider> </Grid> </Container> ); } export default App;
Теперь нам нужно изменить наши компоненты ‹Input_one /› и ‹Input_two /›.
Все последующие изменения будут полностью одинаковыми для обоих входных компонентов. Сначала нам нужно импортировать useContext
из react.
import React, { useContext } from 'react';
Теперь нам нужно импортировать AppContext из компонента приложения.
import { AppContext } from '../App'
Чтобы использовать наши значения состояния внутри компонента, нам нужно добавить
const {state, dispatch} = useContext(AppContext);
Затем добавьте функцию, которая будет обновлять значение состояния inputText
с помощью dispatch.
. Эта функция будет вызываться каждый раз, когда мы вводим что-то в любой ввод.
const changeInputValue = (newValue) => { dispatch({ type: 'UPDATE_INPUT', data: newValue,}); };
Теперь нам нужно установить value в state.inputText, и событие onChange должно вызывать функцию changeInputValue
.
Теперь ./src/components/Input_one.js
должен выглядеть так:
import React, { useContext } from 'react'; import {makeStyles} from "@material-ui/core/styles/index"; //Material UI components import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; //Material UI Icons import IconButton from '@material-ui/core/IconButton'; import SearchIcon from '@material-ui/icons/Search'; // Import Context import { AppContext } from '../App' const useStyles = makeStyles({ root: { padding: '2px 4px', display: 'flex', alignItems: 'center', width: '100%', }, input: { marginLeft: '8px', flex: 1, }, iconButton: { padding: 10, } }); export default function Input_one() { const classes = useStyles(); const {state, dispatch} = useContext(AppContext); const changeInputValue = (newValue) => { dispatch({ type: 'UPDATE_INPUT', data: newValue,}); }; return( <React.Fragment> <Grid item xs={12} md={6}> <Paper className={classes.root}> <InputBase className={classes.input} placeholder="Input one" value={state.inputText} onChange={e => changeInputValue(e.target.value)} /> <IconButton className={classes.iconButton} aria-label="search"> <SearchIcon /> </IconButton> </Paper> </Grid> </React.Fragment> ) }
И ./src/components/Input_two.js
должен выглядеть так:
import React, { useContext } from 'react'; import {makeStyles} from "@material-ui/core/styles/index"; //Material UI components import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; //Material UI Icons import IconButton from '@material-ui/core/IconButton'; import SearchIcon from '@material-ui/icons/Search'; //Import Context import { AppContext } from '../App' const useStyles = makeStyles({ root: { padding: '2px 4px', display: 'flex', alignItems: 'center', width: '100%', }, input: { marginLeft: '8px', flex: 1, }, iconButton: { padding: 10, } }); export default function Input_two() { const classes = useStyles(); const {state, dispatch} = useContext(AppContext); const changeInputValue = (newValue) => { dispatch({ type: 'UPDATE_INPUT', data: newValue,}); }; return( <React.Fragment> <Grid item xs={12} md={6}> <Paper className={classes.root}> <InputBase className={classes.input} placeholder="Input two" value={state.inputText} onChange={e => changeInputValue(e.target.value)} /> <IconButton className={classes.iconButton} aria-label="search"> <SearchIcon /> </IconButton> </Paper> </Grid> </React.Fragment> ) }
Теперь введите что-нибудь в любое поле ввода, и вы увидите, что оба ввода будут обновлены и будут содержать одинаковые значения.
Добавить неизменяемость-помощник
Итак, теперь наше приложение работает так, как мы ожидали. Но давайте обновим наше начальное состояние в ./src/App.js
, добавим еще одно значение и отобразим его на странице. Итак, теперь наш объект initialState должен быть таким:
// Set up Initial State const initialState = { inputText: '', testText: 'Hello world' };
Теперь давайте отобразим testText
на нашей странице. Наш ./src/App.js
файл должен быть таким:
import React, { useReducer } from 'react'; import { makeStyles } from '@material-ui/core/styles'; //Material UI components import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import CssBaseline from '@material-ui/core/CssBaseline'; import Toolbar from '@material-ui/core/Toolbar'; //Import our Inputs import Input_one from './components/Input_one'; import Input_two from './components/Input_two'; //Styles const useStyles = makeStyles({ toolbarTitle: { flex: 1, }, }); // Create context object export const AppContext = React.createContext(); // Set up Initial State const initialState = { inputText: '', testText: 'Hello world' }; function reducer(state, action) { switch (action.type) { case 'UPDATE_INPUT': return { inputText: action.data }; default: return initialState; } } function App() { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); return ( <Container maxWidth="lg"> <CssBaseline /> {/*Title*/} <Toolbar> <Typography component="h2" variant="h5" color="inherit" align="center" noWrap className={classes.toolbarTitle} > Pass data between react sibling components </Typography> </Toolbar> {/*Inputs*/} <Grid container spacing={1}> <AppContext.Provider value={{ state, dispatch }}> <Input_one/> <Input_two/> </AppContext.Provider> </Grid> {/*display testText value*/} <Toolbar> <Typography component="h2" variant="h5" color="inherit" align="center" noWrap className={classes.toolbarTitle} > {state.testText} </Typography> </Toolbar> </Container> ); } export default App;
Если все в порядке, теперь вы должны увидеть текст «Hello world» под нашими полями. Начните вводить что-нибудь в любом поле ввода. Вы можете увидеть, что произошло. Наш текст «Привет, слово» исчез.
Это произошло потому, что в настоящее время каждый раз, когда вы вводите что-либо в любой ввод, вы полностью заменяете наш объект initialState только на inputText
новое состояние. Но есть решение, как это исправить. Нам не нужно каждый раз заменять наш объект initialState, нам просто нужно обновить значение inputText
и не касаться других состояний. Для этого мы можем использовать библиотеку immutability-helper, которая очень хорошо документирована. Установите эту библиотеку, набрав команду в своем терминале
npm install immutability-helper --save
Теперь нам нужно просто импортировать эту библиотеку в наш ./src/App.js
файл и обновить функцию reducer
в соответствии с документацией по поддержке неизменяемости. Итак, наш ./src/App.js
файл должен выглядеть так:
import React, { useReducer } from 'react'; import { makeStyles } from '@material-ui/core/styles'; //Import immutability-helper import update from 'immutability-helper'; //Material UI components import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import CssBaseline from '@material-ui/core/CssBaseline'; import Toolbar from '@material-ui/core/Toolbar'; //Import our Inputs import Input_one from './components/Input_one'; import Input_two from './components/Input_two'; //Styles const useStyles = makeStyles({ toolbarTitle: { flex: 1, }, }); // Create context object export const AppContext = React.createContext(); // Set up Initial State const initialState = { inputText: '', testText: 'Hello world' }; function reducer(state, action) { switch (action.type) { case 'UPDATE_INPUT': return update(state, { inputText: {$set: action.data}}); default: return initialState; } } function App() { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); return ( <Container maxWidth="lg"> <CssBaseline /> {/*Title*/} <Toolbar> <Typography component="h2" variant="h5" color="inherit" align="center" noWrap className={classes.toolbarTitle} > Pass data between react sibling components </Typography> </Toolbar> {/*Inputs*/} <Grid container spacing={1}> <AppContext.Provider value={{ state, dispatch }}> <Input_one/> <Input_two/> </AppContext.Provider> </Grid> {/*display testText value*/} <Toolbar> <Typography component="h2" variant="h5" color="inherit" align="center" noWrap className={classes.toolbarTitle} > {state.testText} </Typography> </Toolbar> </Container> ); } export default App;
Когда вы внесете эти изменения, ваше приложение должно работать должным образом. Если вы напечатаете что-то в любом поле ввода, заголовок «Hello world» по-прежнему должен быть на странице. Теперь мы обновляем только значения, с которыми работаем, а НЕ заменяем все объекты.
Резюме
Весь окончательный код этого приложения вы можете найти в моем репозитории GitHub. Как видите, теперь передача данных между родственными компонентами React становится простой. Если вы знакомы с Redux, вы можете увидеть много знакомых вещей. Context Api позволяет нам обмениваться данными, которые можно считать глобальными для дерева компонентов React. Это решение очень помогает в оптимизации нашего приложения React, но для более сложных проектов с несколькими уровнями вложенности я предпочитаю использовать Redux. Если вы справитесь со всем этим процессом, как я вам показал, то вы тоже легко поймете Redux.
Я надеюсь, что эта статья вам очень поможет. Мне любопытно узнать и о других интересных способах сделать это. Пожалуйста, дайте мне знать, что вы думаете, в комментариях ниже.
🎉Счастливое кодирование🎉