Как передавать данные между компонентами в ReactJS

2 года назад·3 мин. на чтение

В React можно разными способами передавать данные между компонентами. Применимость каждого способа определяется направлением движения данных. Данные могут двигаться от дочернего компонента к родительскому или наоборот. Данные могут двигаться глубоко - от корневого элемента до элемента-потомка. Данными могут обмениваться соседние элементы. В этой статье мы рассмотрим как передавать данные в каждом конкретном случае.

Есть несколько типичных ситуаций передачи данные между компонентами в React:
  • от родительского компонента к дочернему;
  • от дочернего компонента к родительскому;
  • между соседними компонентами;
  • от компонента к компоненту-потомку (через несколько уровней вниз);
  • от компонента к компоненту-предку (через несколько уровней вверх).
Данные можно передавать:
  • через пропсы;
  • используя callback-функцию;
  • пробросом пропсов от уровня к уровню (prop drilling);
  • при помощи контекста (React Context AP);
  • через хранилище (store);
На практике также приходится передавать данные не только между компонентами, но и между страницами. Способы передачи данных между компонентами, находящимися на разных страницах, с помощью React Router можно прочитать в статье Полное руководство по React Router v6. Часть 3 - Управление навигацией.

От родительского компонента к дочернему

Наиболее простой и часто встречающийся случай - это случай, когда дочерний компонент принимает данные от родителя через пропсы.
import { useState } from 'react'

const Parent = () => {
  const [value, setValue] = useState('')

  const handleChange = (event) => {
    setValue(event.target.value)
  }

  return (
    <div>
       <input
         type="text"
         onChange={handleChange }
       />
      {/* передаем проп в дочерний компонент */}
      <Child value={value} />
    </div>
  )
}

const Child = ({ value }) => {
  return (
    <span>Value is: {value || '<Not set>'}</span>
  )
}

От дочернего компонента к родительскому

Если необходимо передать данные от дочернего реакт компонента к родительскому, используются функции обратного вызова (callback-функции).
import { useState } from 'react'

const Child = ({ onChange }) => {
  const handleChange = (event) => {
    onChange(event.target.value) // callback-функция
  }

  return (
    <input
      type="text"
      onChange={handleChange}
    />
  )
}

const Parent = () => {
  const [value, setValue] = useState('')
  const handleChange = (value) => {
    setValue(value)
  }

  return (
    <div>
      <span>Value is: {value || '<Not set>'}</span>
      <Child onChange={handleChange} />
    </div>
  )
}

Между соседними компонентами

Данные между соседними компонентами, т.е. между компонентами на одном уровне, можно передать через общий предок. Обычно данные от одного Реакт компонента передаются вверх, в компонент-предок, через callback-функцию, а компонент-предок передает их в другой компонент через проп.
import { useState } from 'react'

const Parent = () => {
  const [value, setValue] = useState('')

  const handleChange = (value) => {
    setValue(value)
  }

  return (
    <div>
      <Sibling1 onChange={handleChange} />
      <Sibling2 value={value} />
    </div>
  )
}


const Sibling1 = ({ onChange }) => {
  const handleChange = (event) => {
    onChange(event.target.value)
  }

  return (
    <input
      type="text"
      onChange={handleChange}
    />
  )
}

const Sibling2 = ({ value}) => {
  return (
    <span>Value is: {value || '<Not set>'}</span>
  )
}

Через несколько уровней вверх/вниз

Если компоненты находится в несколько уровнях друг от друга, то также можно передать проп. Этот проп придется описывать во всех компонентах на всех промежуточных уровнях. Эта ситуация называется 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 (
    <input
      type="text"
      onChange={handleChange}
    />
  )
}

// Component2 читает данные из контекста ValueContext
const Component2 = () => {
  const { value } = useContext(ValueContext)

  return (
    <span>Value is: {value || '<Not set>'}</span>
  )
}

// компоненты, которым необходим доступ к контексту,
// должны быть обернуты в Provider
export default function App() {
  const [value, setValue] = useState('')

  return (
    <ValueContext.Provider value={{ value, setValue }}>
      <Component1 />
      <Component2 />
    </ValueContext.Provider>
  )
}
Исходный код Подробное руководство по React

Что нужно знать о Redux - action/dispatch/reducer/store

2 года назад·3 мин. на чтение

В этой статье рассмотрим работу с Redux - его основные понятия, и как Redux работает с данными.

Redux - это стэйт менеджер для JS приложений. Он может работать не только в React приложениях. Просто так сложилось исторически, что они часто упоминаются вместе. Redux хранит состояние в дереве объектов внутри единого стора. Единственная возможность изменить состояние - отправить action. Action это объект, который описывает действие. Он как бы отвечает на вопрос "Что я хочу изменить в состоянии?".
// types.js
const ADD_TODO = "ADD_TODO"
// actions.js
import { ADD_TODO } from "./types.js"

export const addTodoAction = {
  type: ADD_TODO,
  payload: {
     id: 1,
     text: "Изучить Redux",
     done: false
   }
}
Далее action попадает в reducer, где описано как состояние должно быть изменено ("Как я хочу изменить состояние?").
//  todo-reducer.js
import { ADD_TODO } from "./types.js"

export const todoReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO: {
      const item = action.payload
      return [...state, item]
    }
    default:
      return state
  }
}

Для чего нужен Redux?

Redux и в целом любая другая система работы с состоянием нужна для контроля над состоянием. Веб приложения усложняются, добавляются новые фичи и контроль над приложением теряется. Становится сложно охватывать большой проект как единое целое. Становится cложно передавать изменения. Встроенными способами в React их приходилось бы передавать по цепочке от одного компонента в другой или через Context API. По дороге обновленное состояние может также влиять на другие компоненты и т.д. И такая бесконтрольность расползается по всему проекту и может послужить появлению новых багов и добавит сложность в отладке. Теряется прозрачность того что происходит.
Action’ы добавляют порядок в происходящие изменения. Action - это объект, и его можно залогировать и последовательно отслеживать как меняется состояние. Подытожив, можно вывести три принципа Redux:
  • Единственный источник правды - единый стор, который меняются и всегда содержит актуальные данные.
  • Состояние - только для чтения. Нельзя менять его напрямую - только через action’ы.
  • Изменения делаются только при помощи чистых функций.
  • Эти функции (редьюсеры) принимают в качестве аргумента старое состояние и возвращают обновленное состояние. И всегда при одних и тех же данных результат этих функций будет одинаков.

Понятия в Redux

Action

Action (экшн) - это источник данных в стор, Action включает тип и некоторую информацию (payload). Тип обычно имеет строковое значение. Он нужен чтобы reducer понимал какой action был отправлен. Далее к стору будет применен payload.

Reducer

Reducer (редьюсер) - это функция, которая определяет как должно меняться состояние в зависимости от аction’а. Редьюсеры должны быть чистым функциями - они должны вычислить следующее состояние и вернуть новый объект состояния. Никаких сайд эффектов, мутаций состояния, и вызовов API в редьюсере быть не должно.

Store

Store - объединяет аction’ы и редьюсеры
  • Хранит состояние приложения.
  • Предоставляет доступ к состоянию через getState.
  • Позволяет изменять состояние через dispatch.
  • Регистрирует подписчиков через subscribe.
Теперь когда мы знаем основные понятия сформулируем флоу - движение данных в Redux. Сначала вызываем функцию dispatch, передав в нее action.
// App.jsx
import { useDispatch } from "react-redux"
import { ADD_TODO } from "./types.js"

export default App() {
  const dispatch = useDispatch()
 
  const handleTodoAdd = () => {
    dispatch({
      type: ADD_TODO,
       payload: {
         id: 1,
         text: "Изучить Redux",
         done: false
       },
    })
  }

  return (
    <button type="button" onClick={handleTodoAdd}>Добавить todo</button>
  )
}
Далее Redux вызывает переданный в него редьюсер. В редьюсер отправляется два аргумента - текущее состояние (state) и action.
//  todo-reducer.js
import { ADD_TODO } from "./types.js"

export const todoReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO: {
      const item = action.payload
      return [...state, item]
    }
    default:
      return state
  }
}
Redux сохраняет весь объект, который вернулся из корневого редьюсера. Почему редьюсер - корневой? В Redux передается один редьюсер, но его можно разделить на несколько и собрать вместе при помощи combineReducers. Результатом этой функции будет корневой редьюсер. Обычно это делается, когда в приложении есть несколько модулей и есть смысл разделить большой редьюсер на несколько маленьких. Но для мелких приложений это совсем не обязательно.
// reducers.js
import { combineReducers } from "redux"
import { todoReducer } from "./todo-reducer"

export const rootReducer = combineReducers({
  todos: todoReducer,
})
Как уже упоминалось, Redux работает не только с ReactJS. Это универсальный инструмент. Однако для того чтобы использовать его в ReactJS нужен специальный байндинг, который ставится как отдельный пакет - react-redux.

Как подключить Redux к ReactJS проекту

Установить Redux с помощью команды.
npm i redux react-redux
Проинициализировать store и обернуть приложение тегом <Provider> c переданным в него объектом store.
// index.js
import React from "react"
import ReactDOM from "react-dom/client"

import { createStore } from "redux"
import { Provider } from "react-redux"

import App from "./App"
import { rootReducer } from "./reducers"

const store = createStore(rootReducer)

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);