Я создам простое приложение для реагирования с двумя входами, где их значения будут полностью синхронизированы. В этой статье я объясню, как легко отправлять данные между одноуровневыми реагирующими компонентами, используя самые последние функции реагирования.

Настроить среду 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.

Я надеюсь, что эта статья вам очень поможет. Мне любопытно узнать и о других интересных способах сделать это. Пожалуйста, дайте мне знать, что вы думаете, в комментариях ниже.

🎉Счастливое кодирование🎉