¿Cuál es la forma correcta de pasar el estado del elemento de formulario a elementos hermanos / primarios?

186
  • Supongamos que tengo una clase React P, que representa dos clases secundarias, C1 y C2.
  • C1 contiene un campo de entrada. Me referiré a este campo de entrada como Foo.
  • Mi objetivo es dejar que C2 reaccione a los cambios en Foo.

Se me ocurrieron dos soluciones, pero ninguna de ellas se siente del todo bien.

Primera solución

  1. Asignar P un estado, state.input.
  2. Cree una onChangefunción en P, que toma un evento y establece state.input.
  3. Pase esto onChangea C1 como a props, y deje que C1 se una this.props.onChangeal onChangeFoo.

Esto funciona. Cada vez que cambia el valor de Foo, se activa a setStateen P, por lo que P tendrá la entrada para pasar a C2.

Pero no se siente del todo bien por la misma razón: estoy configurando el estado de un elemento padre a partir de un elemento hijo. Esto parece traicionar el principio de diseño de React: flujo de datos de una sola dirección.
¿Es así como se supone que debo hacerlo, o hay una solución más React-natural?

Segunda solución:

Solo pon a Foo en P.

Pero, ¿es este un principio de diseño que debo seguir al estructurar mi aplicación, colocando todos los elementos del formulario en la renderclase de más alto nivel?

Al igual que en mi ejemplo, si tengo una gran representación de C1, realmente no quiero poner el conjunto renderde C1 a renderP solo porque C1 tiene un elemento de forma.

¿Cómo debería hacerlo?

octref
fuente
Estoy a punto de hacer exactamente lo mismo y, a pesar de que funciona correctamente, tengo la sensación de que es solo un truco gigante
Mirko

Respuestas:

198

Entonces, si te entiendo correctamente, ¿tu primera solución sugiere que mantienes el estado en tu componente raíz? No puedo hablar por los creadores de React, pero en general, encuentro que esta es una solución adecuada.

Mantener el estado es una de las razones (al menos eso creo) de que se creó React. Si alguna vez ha implementado su propio patrón de estado del lado del cliente para lidiar con una interfaz de usuario dinámica que tiene muchas piezas móviles interdependientes, entonces le encantará Reaccionar, porque alivia mucho este dolor de administración del estado.

Al mantener el estado más arriba en la jerarquía y actualizarlo a través de eventos, su flujo de datos sigue siendo bastante unidireccional, solo está respondiendo a eventos en el componente raíz, realmente no está obteniendo los datos allí a través de un enlace bidireccional, le está diciendo al componente Root que "oye, algo sucedió aquí abajo, revisa los valores" o estás pasando el estado de algunos datos en el componente hijo para actualizar el estado. Cambió el estado en C1 y desea que C2 lo tenga en cuenta, por lo tanto, al actualizar el estado en el componente raíz y volver a renderizar, los accesorios de C2 ahora están sincronizados ya que el estado se actualizó en el componente raíz y se transmitió .

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)
captray
fuente
55
No hay problema. De hecho, volví después de escribir esta publicación y releí parte de la documentación, y parece estar en línea con su pensamiento y mejores prácticas. React tiene una documentación realmente excelente, y cada vez que termino preguntándome a dónde debería ir algo, normalmente lo tienen cubierto en algún lugar de los documentos. Echa un vistazo a la sección sobre estado aquí, facebook.github.io/react/docs/…
captray
@captray, pero ¿qué pasa si C2tiene un getInitialStatefor for data y dentro de renderél this.state.data?
Dmitry Polushkin
2
@DmitryPolushkin Si entiendo su pregunta correctamente, desea pasar los datos de su componente raíz a C2 como accesorios. En C2, esos datos se establecerían como estado inicial (es decir, getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}} y querrás implementar componentWillReceiveProps y llamar a this.setState con los nuevos accesorios para actualizar el . estado en C2 Desde que inicialmente respondí, he estado utilizando Flux, y lo recomiendo encarecidamente que se mire, así que hace que su limpiador de componentes y va a cambiar la manera de pensar acerca del estado..
captray
3
@DmitryPolushkin Quería publicar esto en el seguimiento. facebook.github.io/react/tips/… Está bien siempre y cuando sepas lo que estás haciendo y sepas que los datos van a cambiar, pero en muchas situaciones probablemente puedas mover las cosas. También es importante tener en cuenta que no tiene que construir esto como una jerarquía. Puede montar C1 y C2 en diferentes lugares del DOM, y ambos pueden escuchar eventos de cambio en algunos datos. Veo a muchas personas presionando por componentes jerárquicos cuando no los necesitan.
captray
55
El código anterior tiene 2 errores, los cuales implican no vincular "esto" en el contexto correcto, he realizado las correcciones anteriores y también para cualquiera que necesite una demostración de codepen
Alex H
34

Habiendo usado React para construir una aplicación ahora, me gustaría compartir algunas ideas sobre esta pregunta que hice hace medio año.

Te recomiendo que leas

La primera publicación es extremadamente útil para comprender cómo debe estructurar su aplicación React.

Flux responde a la pregunta de por qué debería estructurar su aplicación React de esta manera (en lugar de cómo estructurarla). React es solo el 50% del sistema, y ​​con Flux puedes ver la imagen completa y ver cómo constituyen un sistema coherente.

De vuelta a la pregunta.

En cuanto a mi primera solución, está totalmente bien dejar que el controlador vaya en la dirección inversa, ya que los datos siguen yendo en una sola dirección.

Sin embargo, si permitir que un controlador active un setState en P puede ser correcto o incorrecto dependiendo de su situación.

Si la aplicación es un simple convertidor Markdown, C1 es la entrada sin procesar y C2 es la salida HTML, está bien permitir que C1 active un setState en P, pero algunos podrían argumentar que esta no es la forma recomendada de hacerlo.

Sin embargo, si la aplicación es una lista de tareas, C1 ser la entrada para la creación de un nuevo TODO, C2 la lista de tareas en HTML, es probable que desee manejador para ir hasta dos niveles de P - a la dispatcherque dejó que el storeactualización de la data store, que luego envían los datos a P y pueblan las vistas. Ver ese artículo de Flux. Aquí hay un ejemplo: Flux - TodoMVC

En general, prefiero la forma descrita en el ejemplo de la lista de tareas pendientes. Cuanto menos estado tenga en su aplicación, mejor.

octref
fuente
77
Frecuentemente discuto esto en presentaciones sobre React y Flux. El punto que trato de enfatizar es lo que ha descrito anteriormente, y esa es la separación de View State y Application State. Hay situaciones en las que las cosas pueden pasar de simplemente Ver estado a convertirse en Estado de aplicación, especialmente en situaciones en las que está guardando el estado de la IU (como los valores preestablecidos). Creo que es increíble que volvieras con tus propios pensamientos un año después. +1
captray
@captray Entonces, ¿podemos decir que redux es poderoso que reaccionar en todas las circunstancias? A pesar de su curva de aprendizaje ... (viniendo de Java)
Bruce Sun
No estoy seguro de lo que quieres decir. Manejan muy bien dos cosas diferentes y son una separación adecuada de las preocupaciones.
captray
1
Tenemos un proyecto que usa redux, y meses después, parece que las acciones se usan para todo. Es como esta gran mezcla de código de espagueti y un desastre total imposible de entender. La gestión estatal puede ser buena, pero posiblemente gravemente mal entendida. ¿Es seguro asumir que flux / redux solo debe usarse para partes del estado a las que se debe acceder globalmente?
wayofthefuture
1
@wayofthefuture Sin ver su código, puedo decir que veo muchos espaguetis Reaccionar como un buen spaghetti jQuery. El mejor consejo que puedo ofrecer es intentar seguir SRP. Haga que sus componentes sean lo más simples posible; componentes de representación tontos si puedes. También presiono por abstracciones como un componente <DataProvider />. Hace una cosa bien. Proveer información. Esto generalmente se convierte en el componente 'raíz' y pasa los datos al aprovechar a los hijos (y la clonación) como accesorios (contrato definido). En última instancia, intente pensar primero en el servidor. Hará que su JS sea mucho más limpio con mejores estructuras de datos.
captray
6

Cinco años después, con la introducción de React Hooks, ahora hay una forma mucho más elegante de hacerlo con useContext hook.

Defina el contexto en un ámbito global, exporte variables, objetos y funciones en el componente principal y luego ajuste los elementos secundarios en la aplicación en un contexto proporcionado e importe lo que necesite en los componentes secundarios. A continuación se muestra una prueba de concepto.

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);

Puede ejecutar este ejemplo en el editor de Code Sandbox.

Editar paso-estado-con-contexto

J. Incorrecto
fuente
5

La primera solución, con mantener el estado en el componente principal , es la correcta . Sin embargo, para problemas más complejos, debe pensar en alguna biblioteca de administración de estado , redux es el más popular utilizado con react.

Nesha Zoric
fuente
Convenido. Mi respuesta fue escrita cuando la mayoría escribía cosas en "reacción pura". Antes de la explosión del flujo.
Captray
2

Me sorprende que no haya respuestas con una solución React idiomática directa en el momento en que estoy escribiendo. Así que aquí está el uno (compare el tamaño y la complejidad con otros):

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>
);

Estoy configurando el estado de un elemento primario a partir de un elemento secundario. Esto parece traicionar el principio de diseño de React: flujo de datos de una sola dirección.

Cualquier controlinput (forma idiomática de trabajar con formularios en React) actualiza el estado principal en su onChangedevolución de llamada y aún no traiciona nada.

Mire cuidadosamente el componente C1, por ejemplo. ¿Ve alguna diferencia significativa en la forma C1en que un inputcomponente integrado maneja los cambios de estado? No deberías, porque no hay ninguno. Levantar el estado y pasar pares valor / onChange es idiomático para React sin procesar. No uso de referencias, como sugieren algunas respuestas.

Gaperton
fuente
1
¿Qué versión de react estás usando? Tengo problemas de propiedad de clase experimental y foo no está definido.
Isaac Pak
No se trataba de una versión de reaccionar. El código del componente estaba equivocado. Solucionado, intente ahora.
gaperton
Obviamente, debe extraer el miembro de estado this.state, faltaba el retorno en el renderizado y varios componentes deben estar envueltos en div o algo así. No sé cómo me perdí eso cuando escribí la respuesta original. Debe haber sido el error de edición.
gaperton
1
Me gusta esta solución Si alguien quiere jugar con él, aquí hay una caja de arena para usted.
Isaac Pak
2

Respuesta más reciente con un ejemplo, que utiliza React.useState

Mantener el estado en el componente principal es la forma recomendada. El padre debe tener acceso a él, ya que lo gestiona a través de dos componentes secundarios. No se recomienda moverlo al estado global, como el administrado por Redux, por la misma razón por la cual la variable global es peor que la local en general en ingeniería de software.

Cuando el estado está en el componente padre, el niño puede mutarlo si el padre le da al niño valuey al onChangemanejador en accesorios (a veces se llama enlace de valor o patrón de enlace de estado ). Así es como lo haría con ganchos:


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>
}

Todo el componente principal se volverá a representar en el cambio de entrada en el elemento secundario, lo que podría no ser un problema si el componente principal es pequeño / rápido de volver a representar. El rendimiento de representación del componente principal todavía puede ser un problema en el caso general (por ejemplo, formularios grandes). Este es un problema resuelto en su caso (ver más abajo).

El patrón de enlace de estado y la reproducción sin padre son más fáciles de implementar usando la biblioteca de terceros, como Hookstate , sobrealimentada React.useStatepara cubrir una variedad de casos de uso, incluido el suyo. (Descargo de responsabilidad: soy un autor del proyecto).

Así es como se vería con Hookstate. Child1cambiará la entrada, Child2reaccionará a ella. Parentmantendrá el estado pero no se volverá a procesar en el cambio de estado, solo Child1y lo Child2hará.

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>
}

PD: hay muchos más ejemplos aquí que cubren escenarios similares y más complicados, incluidos datos profundamente anidados, validación de estado, estado global con setStategancho, etc. También hay una aplicación de muestra completa en línea , que utiliza el Hookstate y la técnica explicada anteriormente.

Andrés
fuente
1

Debería aprender la biblioteca Redux y ReactRedux. Estructurará sus estados y accesorios en una tienda y podrá acceder a ellos más adelante en sus componentes.

Ramin Taghizada
fuente
1

Con React> = 16.3 puede usar ref y forwardRef, para obtener acceso al DOM del niño desde su padre. No uses más los viejos métodos de referencia.
Aquí está el ejemplo usando su caso:

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>
    }
}

Consulte Refs y ForwardRef para obtener información detallada sobre las referencias y forwardRef.

Lex Soft
fuente
0
  1. Lo correcto es tener el estado en el componente padre , para evitar la referencia y lo que no
  2. Un problema es evitar actualizar constantemente a todos los niños al escribir en un campo
  3. Por lo tanto, cada hijo debe ser un Componente (como no un PureComponent) e implementar shouldComponentUpdate(nextProps, nextState)
  4. De esta manera, al escribir en un campo de formulario, solo ese campo se actualiza

El siguiente código utiliza @boundanotaciones de ES.Next babel-plugin-transform-decorators-legacy de BabelJS 6 y propiedades de clase (la anotación establece este valor en funciones miembro similares a bind):

/*
© 2017-present Harald Rudell <[email protected]> (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} />
  }
}
Harald Rudell
fuente
0

Se explica el concepto de pasar datos de padres a hijos y viceversa.

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"));

akshay
fuente
1
Le sugiero que informe al propietario de la respuesta anterior como un comentario para agregar la descripción proporcionada por usted en su respuesta. Seguido refiriendo su nombre por cortesía.
Thomas Easo
@ThomasEaso No puedo encontrarlo aquí, lo revisé. ¿Hay alguna otra sugerencia
Akshay
haga clic en el botón "editar" a la izquierda de su icono y modifique su respuesta y envíela. irá a un moderador y ellos harán lo necesario por sus valiosos comentarios.
Thomas Easo