Как передавать данные между компонентами в ReactJS
2 года назад·3 мин. на чтение
В React можно разными способами передавать данные между компонентами. Применимость каждого способа определяется направлением движения данных. Данные могут двигаться от дочернего компонента к родительскому или наоборот. Данные могут двигаться глубоко - от корневого элемента до элемента-потомка. Данными могут обмениваться соседние элементы. В этой статье мы рассмотрим как передавать данные в каждом конкретном случае.
Есть несколько типичных ситуаций передачи данные между компонентами в React:
- от родительского компонента к дочернему;
- от дочернего компонента к родительскому;
- между соседними компонентами;
- от компонента к компоненту-потомку (через несколько уровней вниз);
- от компонента к компоненту-предку (через несколько уровней вверх).
- через пропсы;
- используя callback-функцию;
- пробросом пропсов от уровня к уровню (prop drilling);
- при помощи контекста (React Context AP);
- через хранилище (store);
От родительского компонента к дочернему
Наиболее простой и часто встречающийся случай - это случай, когда дочерний компонент принимает данные от родителя через пропсы.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 и т.д.Исходный код Подробное руководство по Reactimport { 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> ) }
Что нужно знать о Redux - action/dispatch/reducer/store
2 года назад·3 мин. на чтение
В этой статье рассмотрим работу с Redux - его основные понятия, и как Redux работает с данными.
Redux - это стэйт менеджер для JS приложений. Он может работать не только в React приложениях. Просто так сложилось исторически, что они часто упоминаются вместе.
Redux хранит состояние в дереве объектов внутри единого стора. Единственная возможность изменить состояние - отправить action. Action это объект, который описывает действие. Он как бы отвечает на вопрос "Что я хочу изменить в состоянии?".
Далее action попадает в reducer, где описано как состояние должно быть изменено ("Как я хочу изменить состояние?").
Action’ы добавляют порядок в происходящие изменения. Action - это объект, и его можно залогировать и последовательно отслеживать как меняется состояние.
Подытожив, можно вывести три принципа Redux:
Далее Redux вызывает переданный в него редьюсер. В редьюсер отправляется два аргумента - текущее состояние (
// 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 } }
// 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’ы.
- Изменения делаются только при помощи чистых функций.
- Эти функции (редьюсеры) принимают в качестве аргумента старое состояние и возвращают обновленное состояние. И всегда при одних и тех же данных результат этих функций будет одинаков.
Понятия в Redux
Action
Action (экшн) - это источник данных в стор, Action включает тип и некоторую информацию (payload). Тип обычно имеет строковое значение. Он нужен чтобы reducer понимал какой action был отправлен. Далее к стору будет применен payload.Reducer
Reducer (редьюсер) - это функция, которая определяет как должно меняться состояние в зависимости от аction’а. Редьюсеры должны быть чистым функциями - они должны вычислить следующее состояние и вернуть новый объект состояния. Никаких сайд эффектов, мутаций состояния, и вызовов API в редьюсере быть не должно.Store
Store - объединяет аction’ы и редьюсеры- Хранит состояние приложения.
- Предоставляет доступ к состоянию через
getState
. - Позволяет изменять состояние через
dispatch
. - Регистрирует подписчиков через
subscribe
.
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> ) }
state
) и action
.
Redux сохраняет весь объект, который вернулся из корневого редьюсера. Почему редьюсер - корневой? В Redux передается один редьюсер, но его можно разделить на несколько и собрать вместе при помощи// 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 } }
combineReducers
. Результатом этой функции будет корневой редьюсер. Обычно это делается, когда в приложении есть несколько модулей и есть смысл разделить большой редьюсер на несколько маленьких. Но для мелких приложений это совсем не обязательно.
Как уже упоминалось, Redux работает не только с ReactJS. Это универсальный инструмент. Однако для того чтобы использовать его в ReactJS нужен специальный байндинг, который ставится как отдельный пакет -// reducers.js import { combineReducers } from "redux" import { todoReducer } from "./todo-reducer" export const rootReducer = combineReducers({ todos: todoReducer, })
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> );