X

Способы передачи данных между компонентами в React

В React есть несколько способов передачи данных между компонентами: Render props / props, Context, React-Redux / Redux.

В этой статье вы узнаете об этих способах подробно. В каждом из них вы научитесь добиваться двух вариантов взаимодействия:

  • от родителя к наследнику;
  • от наследника к родителю.

Render props / props

Один из самых простых способов передачи данных в компоненты — это props (свойства).

Что такое prop?

Как известно, разметка в компонентах рендерится так:

class C extends React.Component {
   render() {
       return (
           <div>
               C component
           </div>
       )
   }
}
class P extends React.Component {
   render() {
       return (
           <C />
       )
   }
}

Это отобразится как C component

C фактически отображается в P, так как P является его родительским компонентом. Кроме того, P может отобразить C следующим образом:

class P extends React.Component {
   render() {
       return (
           <C name="nnamdi" />
       )
   }
}

P добавляет дополнительные свойства в тег элемента C. Это очень похоже на <div id="d1"> </div> в HTML, где id — это атрибут со значением “nnamdi”. Каждый элемент на самом деле является отдельным экземпляром класса HTMLElement.

Тег <div> анализируется движком рендеринга в браузере, и для каждого тега создаются экземпляры класса HTMLElement. Все теги в HTML имеют соответствующий HTMLELement:

div -> HTMLDivElement
anchor -> HTMLAnchorElement
body -> HTMLBodyElement
button -> HTMLButtonElement
...

Каждый HTML*Element выше является подклассом HTMLElement. Эти HTML*Element имеют методы и свойства, которые можно использовать для манипулирования данными и рендеринга.

HTML*Element
   - appendChild
   - attributes
   - getAttribute
   - hasAttribute
   - innerHTML
   - innerText
   - setAttribute
...

В случае выше будет создан HTMLDivElement:

const divElement = new HTMLDivElement()
divElement.setAttribute("name", "nnamdi")

Имя атрибута будет установлено в объект props вот так:

props = {
   name: "nnamdi"
}

и передастся классу компонента C с помощью React.

class C extends React.Component {
   constructor(props) { }
   ...
}

Таким образом, компонент C может получить доступ к значению name в объекте props через props.name.

class C extends React.Component {
   constructor(props) {
       log(this.props.name) // nnamdi
   }
   ...
}

React основан на JSX, поэтому разметка в P будет скомпилирована в таком виде:

<C name="nnamdi" />
   |
   |
   |
   |
   v
React.createElement(C, { name:"nnamdi" })

Первый параметр — это элемент для рендеринга, а второй содержит атрибуты элемента (в виде объектного литерала). CreateElement возвращает литерал, содержащий первый и второй параметры.

При рендеринге React проверяет, является ли этот литерал на самом деле элементом или же классом. Если это класс, React создаёт его экземпляр, используя ключевое слово new, затем передает этот экземпляр в объект props и вызывает метод render.

const objType = React.createElement(C, { name:"nnamdi" })
if(IS_ELEMENT(objType)) {
   //...
}
else {
   if(IS_CLASS(objType)){
       const klass = objType.component
       // klass теперь содержит класс C, класс С расширяет React.Component {...}`

       const props = objType.props
       // props содержит { name: "nnamdi" }, атрибуты переданы компоненту С в компоненте P ``

       const klzz = new klass(props)
       // здесь klass создаётся и ему передается props.
       klzz.componentWillMount()
       klzz.render()
       klzz.componentDidMount()
       //...
   }
}

Приведённый выше код показывает, что фактически делает React для рендеринга компонента или элемента.

Примечание: фрагмент выше не полный, он нужен, чтобы продемонстрировать, как компоненты получают аргумент props.

Сначала проверяется, является ли objType элементом или классом. Затем IS_ELEMENT(…) проверяет список всех элементов HTML. Если objType соответствует одному из них, то возвращается флаг true: значит objType является элементом. Если IS_ELEMENT(…) возвращает false, то, objType должен быть компонентом класса, тогда он уничтожает props и экземпляр класса из objType. Затем он создаёт новый экземпляр класса, используя ключевое слово new и передавая его в props.

Таким образом P удаётся отправить данные в C.

Но как C теперь может отправлять данные обратно в родительский класс P? Это делается с помощью render props.

Что такое render props? Это концепция, посредством которой функция может быть передана дочерним компонентам в качестве свойства (props). Строка, объект, число или массив могут быть переданы через props дочерним компонентам. Это реализовано следующим образом:

<C func={() => {log("render props")}} />
class C extends React.Component {
   constructor(props){
       this.props.func()
   }
   render() {
       return (
           <div>
               {this.props.func()}
           </div>
       )
   }
}

Здесь функция () => {log ("render props")} передаётся компоненту C через свойство func. Затем компонент C может ссылаться на неё через func в аргументе props. Это функция выводит render props в консоли.

Итак, теперь вы знаете, что такое render props (или свойства для рендеринга). Давайте посмотрим, как дочерние компоненты могут использовать их для связи с родительскими.

class P extends React.Component {
   constructor() {
       this.output = this.output.bind(this)
   }
   output() {
       log("With love from parent P land :)")
   }
   render() {
       return (
           <C func={this.output} />
       )
   }
}
class C extends React.Component {
   constructor(props) {}
   render() {
       return (
           <div>
               C component
               <button onClick={this.props.func}>Send To Parent</button>
           </div>
       )
   }
}

Что происходит в этом фрагменте? У компонента P метод вывода связан с его экземпляром. bind(this) говорит JS запустить функцию внутри области видимости класса, объекта или функции. Вывод здесь выполняется за пределами класса P, но он по-прежнему может ссылаться на все свойства и методы, определённые в P.

Таким образом, метод вывода передаётся в компонент класса C, чтобы получить аргумент props через свойство func.

К кнопке «Send To Parent» компонента C привязано событие onclick, необходимое для вызова метода вывода, с переданным в него аргументом props через func. Так что теперь this.props.func в C содержит метод вывода в классе P:

this.props.func === new P().output

Предположим, кнопка «Send To Parent» нажата. Вызывается this.props.func, ссылаясь на метод вывода в P, далее вызывается сам метод вывода. В консоли будет отображено следующее:

With love from parent P land :)

Теперь C успешно связался со своим родителем P. Можно изменить код, чтобы C отправлял данные в P.

class P extends React.Component {
   constructor() {
       this.output = this.output.bind(this)
       this.state = {
           count: 0
       }
   }
   output(evt) {
       log("With love from parent P land :)")
       this.setState({count: this.state.count + evt})
   }
   render() {
       return (
           <div>
               <h1>Count: {this.state.count}</h1>
               <C func={this.output} />  
           </div>
       )
   }
}
class C extends React.Component {
   constructor(props) {}
   render() {
       return (
           <div>
               C component
               <button onClick={(evt) => this.props.func(Math.random())}>Send To Parent</button>
           </div>
       )
   }
}

В этом фрагменте кода в P добавляется состояние, которое содержит свойство count. В C при нажатии кнопки «Send To Parent» генерируется случайное число и передаётся в this.props.func. Метод вывода компонента P перехватывает значение из аргумента evt и обновляет состояние count полученным значением, вызывая this.setState(…).

Как вы видите, значение P отправилось из его дочернего элемента C.

Свойство Parent можно изменить, передав функцию компоненту Child и вызвав эту функцию внутри компонента Child.

Props и render props — это одно и то же, но они имеют разные концепции. Render props в основном используется для разделения логики рендеринга между компонентами. Но в данном случае этот метод был использован для передачи данных от дочернего компонента к родительскому.

Context

Для большинства разработчиков передача props глубоко вложенным компонентам в дереве была бы утомительной. React предоставляет способ определять глобальные данные в одном месте и получать к ним доступ из любого места в приложении.

Context (контекст) как раз используется в React для обмена данными между глубоко вложенными компонентами.

const ColorContext = React.createContext("yellow")
class App extends React.Component {
   render() {
       return (
           <ColorContext.Provider>
               <P />
           </ColorContext.Provider>
       )
   }
}
class P extends React.Component {
   render() {
       return (
           {this.context}
           <C />
       )
   }
}
class C extends React.Component {
   render() {
       return (
           <Sub-C />
       )
   }
}
class Sub-C extends React.Component {
   render() {
       <div>
           {this.context}
       </div>
   }
}

В этом фрагменте есть три компонента в дереве: App -> P -> C -> Sub-C. App отображает P, P отображает C, а C отображает Sub-C, который определён как context с именем ColorContext. P заключён в ColorContext.Provider, это обеспечивает доступ к данным, определённым в ColorContext, всем дочерним компонентам P.

Дочерние компоненты могут получить доступ к данным, определённым в ColorContext, выполнив {this.context}.

Чтобы сообщить React о намерении передать context от родительского компонента остальным его дочерним элементам, нужно определить два атрибута в родительском классе:

  • childContextTypes,
  • getChildContext.

Чтобы извлечь контекст внутри дочернего компонента, нужно определить в нём contextTypes.

Родительский компонент определяет context, который React использует для передачи данных глобально своим дочерним элементам.

Как теперь сделать так, чтобы дочерние элементы могли передавать данные своим родителям?

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

React-Redux/Redux

Это своего рода “ремейк” способа Context в React. Redux — это простая библиотека управления свойствами приложения, которая позволяет легко хранить эти свойства в одном месте и изменять их из любого места.

React-Redux был создан, чтобы сделать Redux легко совместимым с приложениями React.

Поскольку состояние определяется в одном месте, родительские компоненты могут устанавливать или обновлять его значение, а дочерние могут это фиксировать: связь Родитель-Наследник.

Аналогично дочерние компоненты могут устанавливать или обновлять значение состояния в хранилище, и оно также будет фиксироваться родительскими компонентами: связь Наследник-Родитель.

// ...
const initialState = {
   count: 0
}
const countApp = function(state = initialState, action) {
   switch(action) {
       case 'ADD_COUNT':
           return {...state, state.count++}
       return state
   }
}
const store = createStore(countApp)
// Директория src/P.js
class P extends React.Component {
   render() {
       return (
           <button >Increment count</button>
           <h1>Count: {this.props.count}</h1>
           <C />
       )
   }
}
/**
* отправка maps в props
**/
function mapDispatchToProps(dispatch) {
   return {
       updateCount: (count)=> { dispatch({type: "ADD_COUNT",count})}
   }
}
/**
* maps передаёт свойство в props
**/
function mapStateToProps(state) {
   return {
       count: state.count
   }
}
export default connect(mapStateToProps, mapDispatchToProps)(P)
// Директория src/C.js
class C extends React.Component {
   render() {
       return (
           <button &пt;Increment count</button&пt;
           <h1&пt;Count: {this.props.count}</h1&пt;
       )
   }
}
/**
* отправка maps в props
**/
function mapDispatchToProps(dispatch) {
   return {
       updateCount: (count)=&пt; { dispatch({type: "ADD_COUNT",count})}
   }
}
/**
* maps передаёт свойство в props
**/
function mapStateToProps(state) {
   return {
       count: state.count
   }
}
export default connect(mapStateToProps, mapDispatchToProps)(C)

В этом фрагменте создаётся центральное хранилище, в котором содержится свойство count. P может обновить это свойство и дочерний компонент C получит его. Дочерний компонент также может обновить свойство count, и родительский компонент получит его.

Они могут подключиться к центральному хранилищу, объединившись в функцию connect(...), которая возвращает компонент высшего порядка.

Заключение

В этом материале вы увидели, как происходит взаимодействие между компонентами React и какими способами можно этого достичь:

  • Родитель-наследник,
  • Наследник-Родитель.

В первом разделе вы увидели использование props и render props, они работают с помощью HTML-подобных атрибутов. Context использует центральное хранилище, которое родитель и потомки могут как читать, так и обновлять. React-Redux и Redux делают то же самое, что и Context, компоненты выполняют чтение/запись из центрального хранилища.

Для дальнейшего освоения React вам может быть полезен данный материал.

Перевод статьи React: Communication Between Components

Также рекомендуем:

Рубрика: Переводы
Темы: JavaScriptReactReduxДля продолжающих