Как правильно передать состояние элемента формы родственным / родным элементам?


186
  • Предположим, у меня есть класс React P, который отображает два дочерних класса, C1 и C2.
  • C1 содержит поле ввода. Я буду называть это поле ввода Foo.
  • Моя цель - позволить C2 реагировать на изменения в Foo.

У меня есть два решения, но ни одно из них не кажется правильным.

Первое решение:

  1. Присвоить P состояние, state.input.
  2. Создайте onChangeфункцию в P, которая принимает событие и устанавливает state.input.
  3. Передайте это onChangeC1 как a props, и пусть C1 свяжется this.props.onChangeс onChangeFoo.

Это работает. Всякий раз, когда значение Foo изменяется, оно запускает a setStateв P, поэтому у P будет вход для передачи в C2.

Но это не совсем правильно по той же причине: я устанавливаю состояние родительского элемента из дочернего элемента. Похоже, это нарушает принцип разработки React: однонаправленный поток данных.
Это то, как я должен это делать, или есть более естественное решение React?

Второе решение:

Просто поместите Foo в P.

Но должен ли я следовать этому принципу проектирования при структурировании своего приложения - помещать все элементы формы в renderкласс самого высокого уровня?

Как и в моем примере, если у меня большой рендеринг C1, я действительно не хочу помещать все renderC1 в renderP только потому, что C1 имеет элемент формы.

как мне это сделать?


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

Ответы:


198

Итак, если я правильно вас понимаю, ваше первое решение предполагает сохранение состояния в корневом компоненте? Я не могу говорить за создателей React, но в целом я считаю, что это правильное решение.

Поддержание состояния является одной из причин (по крайней мере, я так думаю) того, что React был создан. Если вы когда-либо реализовывали свою собственную клиентскую сторону шаблона состояний для работы с динамическим пользовательским интерфейсом, который имеет много взаимозависимых движущихся частей, то вам понравится React, потому что он значительно облегчает эту проблему управления состоянием.

Поддерживая состояние в иерархии и обновляя его с помощью событий, ваш поток данных все еще в значительной степени однонаправлен, вы просто реагируете на события в компоненте Root, вы на самом деле не получаете данные там посредством двухстороннего связывания, вы говорите компоненту Root, что «эй, здесь что-то случилось, проверьте значения», или вы передаете состояние некоторых данных в дочерний компонент, чтобы обновить состояние. Вы изменили состояние в C1, и вы хотите, чтобы C2 знал об этом, поэтому, обновляя состояние в Root-компоненте и повторной визуализации, реквизиты C2 теперь синхронизированы, так как состояние было обновлено в Root-компоненте и передано вместе. ,

class Example extends React.Component {
  constructor (props) {
    super(props)
    this.state = { data: 'test' }
  }
  render () {
    return (
      <div>
        <C1 onUpdate={this.onUpdate.bind(this)}/>
        <C2 data={this.state.data}/>
      </div>
    )
  }
  onUpdate (data) { this.setState({ data }) }
}

class C1 extends React.Component {
    render () {
      return (
        <div>
          <input type='text' ref='myInput'/>
          <input type='button' onClick={this.update.bind(this)} value='Update C2'/>
        </div>
      )
    }
    update () {
      this.props.onUpdate(this.refs.myInput.getDOMNode().value)
    }
})

class C2 extends React.Component {
    render () {
      return <div>{this.props.data}</div>
    }
})

ReactDOM.renderComponent(<Example/>, document.body)

5
Нет проблем. Я действительно вернулся после написания этого поста и перечитал некоторую документацию, и это, кажется, соответствует их мышлению и лучшим практикам. У React действительно отличная документация, и каждый раз, когда я задаюсь вопросом, куда что-то нужно, они обычно освещают это где-то в документации. Проверьте раздел о состоянии здесь, facebook.github.io/react/docs/…
captray

@captray, а как насчет, если C2есть getInitialStateдля данных и внутри renderон использует this.state.data?
Дмитрий Полушкин

2
@DmitryPolushkin Если я правильно понимаю ваш вопрос, вы хотите передать данные из корневого компонента в C2 в качестве реквизита. В C2 эти данные будут установлены в качестве начального состояния (т.е. getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}}, и вы захотите реализовать componentWillReceiveProps и вызвать this.setState с новыми реквизитами для обновления состояние в C2. С тех пор, как я первоначально ответил, я использую Flux, и очень рекомендую, чтобы вы тоже посмотрели на него. Это сделает ваши компоненты более чистыми и изменит ваш взгляд на состояние.
captray

3
@DmitryPolushkin Я хотел опубликовать это в продолжение. facebook.github.io/react/tips/… Это нормально, если вы знаете, что делаете, и знаете, что данные будут меняться, но во многих ситуациях вы, вероятно, можете изменить положение вещей. Также важно отметить, что вам не нужно строить это как иерархию. Вы можете смонтировать C1 и C2 в разных местах DOM, и они оба могут прослушивать события изменения некоторых данных. Я вижу, что многие люди настаивают на иерархических компонентах, когда они им не нужны.
captray

5
В приведенном выше коде есть 2 ошибки, каждая из которых связана с необязательным «this» в правильном контексте, я внес исправления выше, а также для всех, кому нужна демонстрация codepen
Алекс Х

34

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

Я рекомендую вам прочитать

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

Flux отвечает на вопрос, почему вы должны структурировать свое приложение React таким образом (в отличие от того, как его структурировать). React - это всего лишь 50% системы, и с Flux вы можете увидеть всю картину и увидеть, как они составляют целостную систему.

Вернуться к вопросу.

Что касается моего первого решения, то вполне нормально, чтобы обработчик шел в обратном направлении, поскольку данные все еще идут в одном направлении.

Однако, может ли разрешение обработчика вызвать setState в P может быть правильным или неправильным в зависимости от вашей ситуации.

Если приложение представляет собой простой конвертер Markdown, C1 является необработанным вводом, а C2 является выводом HTML, то можно разрешить C1 запускать setState в P, но некоторые могут утверждать, что это не рекомендуемый способ сделать это.

Однако, если приложение является списком задач, C1 является входом для создания нового задания, C2 - списком задач в HTML, вы, вероятно, захотите, чтобы обработчик поднялся на два уровня выше, чем P - в dispatcher, что позволяет storeобновлять data store, которые затем отправляют данные в P и заполняют представления. Смотрите эту статью о Flux. Вот пример: Flux - TodoMVC

Как правило, я предпочитаю способ, описанный в примере списка задач. Чем меньше у вас состояния в вашем приложении, тем лучше.


7
Я часто обсуждаю это в презентациях на React и Flux. Я пытаюсь подчеркнуть то, что вы описали выше, и это разделение состояния просмотра и состояния приложения. Существуют ситуации, когда вещи могут перейти от простого состояния просмотра к состоянию приложения, особенно в ситуациях, когда вы сохраняете состояние пользовательского интерфейса (например, предустановленные значения). Я думаю, это здорово, что ты вернулся со своими мыслями год спустя. +1
каптай

@captray Итак, можно ли сказать, что редукс сильнее, чем реагировать на все обстоятельства? Несмотря на их кривую обучения ... (из Java)
Брюс Сан

Я не уверен, что ты имеешь в виду. Они очень хорошо справляются с двумя разными вещами и являются правильным разделением интересов.
captray

1
У нас есть проект, использующий избыточность, и спустя месяцы кажется, что действия используются для всего. Это как большая смесь кода для спагетти и невозможного для понимания, полная катастрофа. Государственное управление может быть хорошим, но, возможно, серьезно неправильно. Безопасно ли предполагать, что поток / избыточность следует использовать только для тех частей штата, к которым требуется глобальный доступ?
путь в будущее

1
@wayofthefuture Не видя ваш код, я могу сказать, что я вижу много спагетти React, как хорошие оле спагетти jQuery. Лучший совет, который я могу предложить, это попытаться следовать SRP. Сделайте ваши компоненты максимально простыми; тупые компоненты рендеринга, если можете. Я также настаиваю на абстракциях, таких как компонент <DataProvider />. Это делает одну вещь хорошо. Предоставить данные. Обычно он становится корневым компонентом и передает данные путем использования дочерних элементов (и клонирования) в качестве реквизита (определенный контракт). В конечном счете, попробуйте сначала подумать о сервере. Это сделает ваш JS намного чище с лучшими структурами данных.
captray

6

Спустя пять лет с введением React Hooks появился намного более элегантный способ сделать это с помощью ловушки useContext.

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

import React, { useState, useContext } from "react";
import ReactDOM from "react-dom";
import styles from "./styles.css";

// Create context container in a global scope so it can be visible by every component
const ContextContainer = React.createContext(null);

const initialAppState = {
  selected: "Nothing"
};

function App() {
  // The app has a state variable and update handler
  const [appState, updateAppState] = useState(initialAppState);

  return (
    <div>
      <h1>Passing state between components</h1>

      {/* 
          This is a context provider. We wrap in it any children that might want to access
          App's variables.
          In 'value' you can pass as many objects, functions as you want. 
           We wanna share appState and its handler with child components,           
       */}
      <ContextContainer.Provider value={{ appState, updateAppState }}>
        {/* Here we load some child components */}
        <Book title="GoT" price="10" />
        <DebugNotice />
      </ContextContainer.Provider>
    </div>
  );
}

// Child component Book
function Book(props) {
  // Inside the child component you can import whatever the context provider allows.
  // Earlier we passed value={{ appState, updateAppState }}
  // In this child we need the appState and the update handler
  const { appState, updateAppState } = useContext(ContextContainer);

  function handleCommentChange(e) {
    //Here on button click we call updateAppState as we would normally do in the App
    // It adds/updates comment property with input value to the appState
    updateAppState({ ...appState, comment: e.target.value });
  }

  return (
    <div className="book">
      <h2>{props.title}</h2>
      <p>${props.price}</p>
      <input
        type="text"
        //Controlled Component. Value is reverse vound the value of the variable in state
        value={appState.comment}
        onChange={handleCommentChange}
      />
      <br />
      <button
        type="button"
        // Here on button click we call updateAppState as we would normally do in the app
        onClick={() => updateAppState({ ...appState, selected: props.title })}
      >
        Select This Book
      </button>
    </div>
  );
}

// Just another child component
function DebugNotice() {
  // Inside the child component you can import whatever the context provider allows.
  // Earlier we passed value={{ appState, updateAppState }}
  // but in this child we only need the appState to display its value
  const { appState } = useContext(ContextContainer);

  /* Here we pretty print the current state of the appState  */
  return (
    <div className="state">
      <h2>appState</h2>
      <pre>{JSON.stringify(appState, null, 2)}</pre>
    </div>
  );
}

const rootElement = document.body;
ReactDOM.render(<App />, rootElement);

Вы можете запустить этот пример в редакторе песочницы кода.

Изменить состояние передачи с контекстом


5

Первое решение с сохранением состояния в родительском компоненте является правильным . Однако, для более сложных проблем, вы должны думать о какой - то государственной библиотеке управления , перевождь является наиболее популярной использовать с среагировать.


Согласовано. Мой ответ был написан обратно, когда большинство писали вещи в «чистом React». До взрыва флюса.
captray

2

Я удивлен, что в данный момент я не отвечаю на идиоматическое решение React. Итак, вот один (сравните размер и сложность с другими):

class P extends React.Component {
    state = { foo : "" };

    render(){
        const { foo } = this.state;

        return (
            <div>
                <C1 value={ foo } onChange={ x => this.setState({ foo : x })} />
                <C2 value={ foo } />
            </div>
        )
    }
}

const C1 = ({ value, onChange }) => (
    <input type="text"
           value={ value }
           onChange={ e => onChange( e.target.value ) } />
);

const C2 = ({ value }) => (
    <div>Reacting on value change: { value }</div>
);

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

Любой контролируемыйinput (идиоматический способ работы с формами в React) обновляет родительское состояние в своем обратном onChangeвызове и по-прежнему ничего не выдает.

Посмотрите внимательно на компонент C1, например. Видите ли вы существенную разницу в том, как C1встроенный inputкомпонент обрабатывает изменения состояния? Вы не должны, потому что нет ни одного. Поднятие состояния и передача пар value / onChange идиоматична для необработанного React. Не использование ссылок, как предполагают некоторые ответы.


1
Какую версию реакции вы используете? Я получаю экспериментальные проблемы со свойствами класса, и foo не определен.
Исаак Пак

Это не было о версии реакции. Код компонента был неверным. Исправлено, попробуйте сейчас.
Гапертон

Очевидно, что вам нужно извлечь элемент состояния this.state, возвращаемое значение отсутствует при рендеринге, и несколько компонентов должны быть заключены в div или что-то еще. Не знаю, как я пропустил это, когда написал оригинальный ответ. Должно быть, это ошибка редактирования.
Гапертон

1
Мне нравится это решение. Если кто-то хочет возиться с этим, вот песочница для вас.
Исаак Пак

2

Более свежий ответ с примером, который использует React.useState

Сохранение состояния в родительском компоненте является рекомендуемым способом. Родитель должен иметь доступ к нему, поскольку он управляет им через два дочерних компонента. Перемещение его в глобальное состояние, подобное тому, которым управляет Redux, не рекомендуется по той же причине, почему глобальная переменная хуже, чем локальная в целом в разработке программного обеспечения.

Когда состояние находится в родительском компоненте, дочерний элемент может изменить его, если родительский элемент передает дочерний элемент valueи onChangeобработчик в реквизитах (иногда это называется ссылкой на значение или шаблоном ссылки на состояние ). Вот как бы вы сделали это с помощью хуков:


function Parent() {
    var [state, setState] = React.useState('initial input value');
    return <>
        <Child1 value={state} onChange={(v) => setState(v)} />
        <Child2 value={state}>
    </>
}

function Child1(props) {
    return <input
        value={props.value}
        onChange={e => props.onChange(e.target.value)}
    />
}

function Child2(props) {
    return <p>Content of the state {props.value}</p>
}

Весь родительский компонент будет повторно отображаться при изменении входных данных в дочернем элементе, что может не быть проблемой, если родительский компонент слишком мал / быстр для повторной визуализации. Производительность повторного рендеринга родительского компонента все еще может быть проблемой в общем случае (например, большие формы). Это решаемая проблема в вашем случае (см. Ниже).

Шаблон связи состояний и отсутствие родительского повторного рендеринга проще реализовать с помощью сторонней библиотеки, такой как Hookstate - с наддувомReact.useState чтобы охватить различные варианты использования, включая ваш. (Отказ от ответственности: я автор проекта).

Вот как это будет выглядеть с Hookstate. Child1изменит вход, Child2отреагирует на это. Parentбудет удерживать состояние, но не будет повторно выполнять рендеринг при изменении состояния, только Child1и Child2будет.

import { useStateLink } from '@hookstate/core';

function Parent() {
    var state = useStateLink('initial input value');
    return <>
        <Child1 state={state} />
        <Child2 state={state}>
    </>
}

function Child1(props) {
    // to avoid parent re-render use local state,
    // could use `props.state` instead of `state` below instead
    var state = useStateLink(props.state)
    return <input
        value={state.get()}
        onChange={e => state.set(e.target.value)}
    />
}

function Child2(props) {
    // to avoid parent re-render use local state,
    // could use `props.state` instead of `state` below instead
    var state = useStateLink(props.state)
    return <p>Content of the state {state.get()}</p>
}

PS: здесь есть еще много примеров, охватывающих аналогичные и более сложные сценарии, включая глубоко вложенные данные, проверку состояния, глобальное состояние с setStateперехватом и т. Д. Кроме того, в Интернете есть полный пример приложения , в котором используется Hookstate и методика, описанная выше.


1

Вы должны изучить библиотеки Redux и ReactRedux. Она структурирует ваши состояния и реквизиты в одном магазине, и вы сможете получить к ним доступ позже в ваших компонентах.


1

С React> = 16.3 вы можете использовать ref и forwardRef, чтобы получить доступ к DOM ребенка от его родителя. Больше не используйте старый способ реферирования.
Вот пример использования вашего случая:

import React, { Component } from 'react';

export default class P extends React.Component {
   constructor (props) {
      super(props)
      this.state = {data: 'test' }
      this.onUpdate = this.onUpdate.bind(this)
      this.ref = React.createRef();
   }

   onUpdate(data) {
      this.setState({data : this.ref.current.value}) 
   }

   render () {
      return (
        <div>
           <C1 ref={this.ref} onUpdate={this.onUpdate}/>
           <C2 data={this.state.data}/>
        </div>
      )
   }
}

const C1 = React.forwardRef((props, ref) => (
    <div>
        <input type='text' ref={ref} onChange={props.onUpdate} />
    </div>
));

class C2 extends React.Component {
    render () {
       return <div>C2 reacts : {this.props.data}</div>
    }
}

См Refs и ForwardRef подробной информации о ссылках и forwardRef.


0
  1. Правильнее всего сделать так, чтобы в родительском компоненте было состояние , чтобы избежать ссылки, а что нет.
  2. Проблема состоит в том, чтобы избежать постоянного обновления всех детей при вводе в поле
  3. Следовательно, каждый дочерний элемент должен быть Компонентом (как в PureComponent) и реализовывать shouldComponentUpdate(nextProps, nextState)
  4. Таким образом, при вводе в поле формы обновляется только это поле

Ниже используются @boundаннотаций от ES.Next babel-plugin-transform-decorators-legacy из BabelJS 6 и класса-свойств (аннотация устанавливает это значение на функции членов , аналогичной затруднительный):

/*
© 2017-present Harald Rudell <harald.rudell@gmail.com> (http://www.haraldrudell.com)
All rights reserved.
*/
import React, {Component} from 'react'
import {bound} from 'class-bind'

const m = 'Form'

export default class Parent extends Component {
  state = {one: 'One', two: 'Two'}

  @bound submit(e) {
    e.preventDefault()
    const values = {...this.state}
    console.log(`${m}.submit:`, values)
  }

  @bound fieldUpdate({name, value}) {
    this.setState({[name]: value})
  }

  render() {
    console.log(`${m}.render`)
    const {state, fieldUpdate, submit} = this
    const p = {fieldUpdate}
    return (
      <form onSubmit={submit}> {/* loop removed for clarity */}
        <Child name='one' value={state.one} {...p} />
        <Child name='two' value={state.two} {...p} />
        <input type="submit" />
      </form>
    )
  }
}

class Child extends Component {
  value = this.props.value

  @bound update(e) {
    const {value} = e.target
    const {name, fieldUpdate} = this.props
    fieldUpdate({name, value})
  }

  shouldComponentUpdate(nextProps) {
    const {value} = nextProps
    const doRender = value !== this.value
    if (doRender) this.value = value
    return doRender
  }

  render() {
    console.log(`Child${this.props.name}.render`)
    const {value} = this.props
    const p = {value}
    return <input {...p} onChange={this.update} />
  }
}

0

Концепция передачи данных от родителя к ребенку и наоборот объясняется.

import React, { Component } from "react";
import ReactDOM from "react-dom";

// taken refrence from https://gist.github.com/sebkouba/a5ac75153ef8d8827b98

//example to show how to send value between parent and child

//  props is the data which is passed to the child component from the parent component

class Parent extends Component {
  constructor(props) {
    super(props);

    this.state = {
      fieldVal: ""
    };
  }

  onUpdateParent = val => {
    this.setState({
      fieldVal: val
    });
  };

  render() {
    return (
      // To achieve the child-parent communication, we can send a function
      // as a Prop to the child component. This function should do whatever
      // it needs to in the component e.g change the state of some property.
      //we are passing the function onUpdateParent to the child
      <div>
        <h2>Parent</h2>
        Value in Parent Component State: {this.state.fieldVal}
        <br />
        <Child onUpdate={this.onUpdateParent} />
        <br />
        <OtherChild passedVal={this.state.fieldVal} />
      </div>
    );
  }
}

class Child extends Component {
  constructor(props) {
    super(props);

    this.state = {
      fieldValChild: ""
    };
  }

  updateValues = e => {
    console.log(e.target.value);
    this.props.onUpdate(e.target.value);
    // onUpdateParent would be passed here and would result
    // into onUpdateParent(e.target.value) as it will replace this.props.onUpdate
    //with itself.
    this.setState({ fieldValChild: e.target.value });
  };

  render() {
    return (
      <div>
        <h4>Child</h4>
        <input
          type="text"
          placeholder="type here"
          onChange={this.updateValues}
          value={this.state.fieldVal}
        />
      </div>
    );
  }
}

class OtherChild extends Component {
  render() {
    return (
      <div>
        <h4>OtherChild</h4>
        Value in OtherChild Props: {this.props.passedVal}
        <h5>
          the child can directly get the passed value from parent by this.props{" "}
        </h5>
      </div>
    );
  }
}

ReactDOM.render(<Parent />, document.getElementById("root"));


1
Я бы предложил вам сообщить предыдущему владельцу ответа в качестве комментария, чтобы добавить описание, предоставленное вами в его ответе. Затем следует ваше имя для любезности.
Томас Исо

@ThomasEaso Я не могу найти его здесь, я проверил это. Есть ли другие предложения
Акшай

нажмите кнопку «Изменить» слева от его значка, измените его ответ и отправьте. он перейдет к модератору, и они сделают все необходимое для ваших ценных комментариев.
Томас Исо
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.