Який правильний спосіб передати стан елементу форми синфайним / батьківським елементам?


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

Я придумав два рішення, але жодне з них не вважає себе цілком правильним.

Перше рішення:

  1. Призначте P a стан state.input,.
  2. Створіть onChangeу P функцію, яка займає подію та встановлює state.input.
  3. Pass це onChangeС1 , як props, і нехай C1 прив'язку this.props.onChangeдо onChangeвзувши.

Це працює. Кожного разу, коли значення Foo змінюється, воно спрацьовує setStateв P, тому P матиме вхід для передачі на C2.

Але це не зовсім правильно з тієї ж причини: я встановлюю стан батьківського елемента з дочірнього елемента. Це, здається, зраджує принципу дизайну React: однобічний потік даних.
Це, як я повинен це зробити, чи є більш реально-природне рішення?

Друге рішення:

Просто поставте Фоо в П.

Але це принцип дизайну, якого я повинен дотримуватися, коли я структурую свою програму - розміщуючи всі елементи форми в 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 Якщо я правильно розумію ваше запитання, ви хочете передати дані з вашого Root Component на C2 як реквізит. У C2 ці дані будуть встановлені як початковий стан (тобто getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}}, і ви захочете реалізувати компонент компонент WillReceiveProps і викликати це.setState з новими реквізитами для оновлення стан C2. Оскільки я спочатку відповів, я використовував Flux, і настійно рекомендую також поглянути на нього. Це робить ваші компоненти більш чистими і змінить те, як ви думаєте про стан.
captray

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

5
У наведеному вище коді є 2 помилки - обидва з яких передбачають не прив'язування "цього" у потрібному контексті, я вніс виправлення вище, а також для всіх, кому потрібна демонстрація кодексу
Алекс Н

34

Скориставшись React для створення програми зараз, я хотів би поділитися думками з цього питання, яке я задав півроку тому.

Рекомендую прочитати

Перша публікація дуже корисна для розуміння того, як слід структурувати додаток React.

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

Повернення до питання.

Що стосується мого першого рішення, то цілком нормально відпустити обробник у зворотному напрямку, оскільки дані все ще йдуть в односторонньому напрямку.

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

Якщо додаток - це звичайний конвертер Markdown, C1 - це вихідний вхід, а C2 - вихід HTML, добре дозволити C1 викликати setState в P, але деякі можуть стверджувати, що це не рекомендований спосіб зробити це.

Однак якщо додаток є списком todo, C1 є вхідним кодом для створення нового todo, C2 список todo в HTML, ви, ймовірно, хочете, щоб обробник перейшов на два рівні вище, ніж P - до dispatcher, що дозволить storeоновити оновлення data store, які потім надсилають дані на P та заповнюють представлення даних. Дивіться цю статтю Flux. Ось приклад: Flux - TodoMVC

Як правило, я віддаю перевагу способу, описаному в прикладі списку todo. Чим менше стан у вашій програмі, тим краще.


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

@captray Отже, чи можна сказати, що редукс потужніший, ніж реагувати за будь-яких обставин? Незважаючи на їх криву навчання ... (що йде з Яви)
Брюс Нд

Я не впевнений, що ти маєш на увазі. Вони дуже добре впораються з двома різними речами і є належним розділенням проблем.
Каптрай

1
У нас є проект, що використовує редукс, і місяці в ньому, здається, ніби дії використовуються для всього. Це як велика суміш коду спагетті і неможливо зрозуміти, тотальна катастрофа. Управління державою може бути гарним, але можливо сильно не зрозумілим. Чи безпечно припустити, що флюс / редукс слід застосовувати лише для частин стану, до яких потрібно отримати доступ у всьому світі?
wayofthefuture

1
@wayofthefuture Не бачачи свого коду, я можу сказати, що я бачу багато спагеті Реагувати так само, як хороші спагетті 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

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


Домовились. Моя відповідь була написана ще тоді, коли більшість писали речі в "чистому реагуванні". До початку флюксплазії.
Каптрай

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компонента? Не слід, бо його немає. Підняття стану та передача пар / значення / зміна пар - ідіоматичне для сировини React. Не використання рефератів, як підказують деякі відповіді.


1
Яку версію реакції ви використовуєте? Я отримую питання про властивості експериментального класу, а foo не визначено.
Ісаак Пак

Мова не йшла про версію реакції. Код компонента був неправильним. Виправлено, спробуйте зараз.
gaperton

Очевидно, що ви повинні вилучити член стану this.state, повернення відсутнє у візуалізації, і кілька компонентів повинні бути загорнуті в діви чи щось. Не знаю, як я пропустив це, коли написав оригінальну відповідь. Повинна була помилка редагування.
gaperton

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
Я б запропонував вам повідомити попереднього власника відповіді як коментар, щоб додати опис, наданий вами у своїй відповіді. Далі слід вказати своє ім’я для ввічливості.
Thomas Easo

@ThomasEaso Я не можу його знайти тут, я перевірив це. Є ще якісь пропозиції
акшай

натисніть кнопку "редагувати" зліва від його значка та змініть його відповідь та надішліть. він перейде до модератора, і він зробить необхідне для ваших цінних коментарів.
Thomas Easo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.