¿Cómo acceder al estado del niño en React?

214

Tengo la siguiente estructura:

FormEditor- contiene múltiples FieldEditor FieldEditor- edita un campo del formulario y guarda varios valores al respecto en su estado

Cuando se hace clic en un botón dentro de FormEditor, quiero poder recopilar información sobre los campos de todos los FieldEditorcomponentes, información que está en su estado, y tenerlo todo dentro de FormEditor.

Pensé en almacenar la información sobre los campos fuera del FieldEditorestado y ponerla en FormEditorsu lugar. Sin embargo, eso requeriría FormEditorescuchar cada uno de sus FieldEditorcomponentes a medida que cambian y almacenan su información en su estado.

¿No puedo acceder al estado de los niños en su lugar? ¿Es ideal?

Moshe Revah
fuente
44
"¿No puedo acceder al estado de los niños? ¿Es ideal?" No. El estado es algo interno y no debe filtrarse al exterior. Puede implementar métodos de acceso para su componente, pero incluso eso no es lo ideal.
Felix Kling
@FelixKling ¿Entonces estás sugiriendo que la forma ideal para la comunicación de niño a padre es solo eventos?
Moshe Revah
3
Sí, los eventos son unidireccionales. O tenga un flujo de datos unidireccional como Flux promueve: facebook.github.io/flux
Felix Kling
1
Si no va a usar FieldEditors por separado, guardar su estado en FormEditorbuen estado. Si este es el caso, sus FieldEditorinstancias se procesarán según lo propsaprobado por su editor de formularios, no por el suyo state. Una forma más compleja pero flexible sería hacer un serializador que pase por cualquier contenedor hijo y encuentre todas las FormEditorinstancias entre ellos y los serialice en un objeto JSON. El objeto JSON se puede anidar opcionalmente (más de un nivel) en función de los niveles de anidación de las instancias en el editor de formularios.
Meglio
44
Creo que React Docs 'Lifting State Up' es probablemente la forma más 'Reacty' de hacer esto
icc97

Respuestas:

135

Si ya tiene el controlador onChange para los FieldEditors individuales, no entiendo por qué no puede simplemente mover el estado al componente FormEditor y simplemente pasar una devolución de llamada desde allí a los FieldEditors que actualizarán el estado principal. Para mí, esa es una forma más Reactiva de hacerlo.

Algo en la línea de esto quizás:

const FieldEditor = ({ value, onChange, id }) => {
  const handleChange = event => {
    const text = event.target.value;
    onChange(id, text);
  };

  return (
    <div className="field-editor">
      <input onChange={handleChange} value={value} />
    </div>
  );
};

const FormEditor = props => {
  const [values, setValues] = useState({});
  const handleFieldChange = (fieldId, value) => {
    setValues({ ...values, [fieldId]: value });
  };

  const fields = props.fields.map(field => (
    <FieldEditor
      key={field}
      id={field}
      onChange={handleFieldChange}
      value={values[field]}
    />
  ));

  return (
    <div>
      {fields}
      <pre>{JSON.stringify(values, null, 2)}</pre>
    </div>
  );
};

// To add abillity to dynamically add/remove fields keep the list in state
const App = () => {
  const fields = ["field1", "field2", "anotherField"];

  return <FormEditor fields={fields} />;
};

Original - versión pre-ganchos:

Markus-ipse
fuente
2
Esto es útil para onChange, pero no tanto en onSubmit.
190290000 Rublo Man
1
@ 190290000RubleMan No estoy seguro de lo que quiere decir, pero dado que los controladores onChange en los campos individuales se aseguran de que siempre tenga los datos del formulario completo disponibles en su estado en su componente FormEditor, su onSubmit solo incluiría algo así myAPI.SaveStuff(this.state);. Si elabora un poco más, tal vez pueda darle una mejor respuesta :)
Markus-ipse
Digamos que tengo un formulario con 3 campos de diferente tipo, cada uno con su propia validación. Me gustaría validar cada uno de ellos antes de enviarlo. Si la validación falla, cada campo que tenga un error debería volver a mostrarse. No he descubierto cómo hacer esto con su solución propuesta, ¡pero tal vez haya una manera!
190290000 Rublo Man
1
@ 190290000RubleMan Parece un caso diferente al de la pregunta original. Abra una nueva pregunta con más detalles y tal vez algún código de muestra, y puedo echar un vistazo más tarde :)
Markus-ipse
Usted puede encontrar esta respuesta útil : utiliza ganchos estatales, que cubre que se debe crear el estado, la forma de evitar la re-forma rinde en cada golpe de teclado, cómo hacer la validación de campos, etc.
Andrew
189

Justo antes de entrar en detalles sobre cómo puede acceder al estado de un componente secundario, asegúrese de leer la respuesta de Markus-ipse con respecto a una mejor solución para manejar este escenario en particular.

Si realmente desea acceder al estado de los elementos refsecundarios de un componente, puede asignar una propiedad llamada a cada elemento secundario. Ahora hay dos formas de implementar referencias: Uso React.createRef()y referencias de devolución de llamada.

Utilizando React.createRef()

Esta es actualmente la forma recomendada de utilizar referencias a partir de React 16.3 (consulte los documentos para obtener más información). Si está utilizando una versión anterior, consulte a continuación las referencias de devolución de llamada.

Deberá crear una nueva referencia en el constructor de su componente principal y luego asignarla a un hijo a través del refatributo.

class FormEditor extends React.Component {
  constructor(props) {
    super(props);
    this.FieldEditor1 = React.createRef();
  }
  render() {
    return <FieldEditor ref={this.FieldEditor1} />;
  }
}

Para acceder a este tipo de referencia, deberá usar:

const currentFieldEditor1 = this.FieldEditor1.current;

Esto devolverá una instancia del componente montado para que pueda usarlo currentFieldEditor1.statepara acceder al estado.

Solo una nota rápida para decir que si usa estas referencias en un nodo DOM en lugar de un componente (por ejemplo <div ref={this.divRef} />) this.divRef.current, devolverá el elemento DOM subyacente en lugar de una instancia de componente.

Referencia de devolución de llamada

Esta propiedad toma una función de devolución de llamada a la que se le pasa una referencia al componente adjunto. Esta devolución de llamada se ejecuta inmediatamente después de que el componente esté montado o desmontado.

Por ejemplo:

<FieldEditor
    ref={(fieldEditor1) => {this.fieldEditor1 = fieldEditor1;}
    {...props}
/>

En estos ejemplos, la referencia se almacena en el componente principal. Para llamar a este componente en su código, puede usar:

this.fieldEditor1

y luego usar this.fieldEditor1.statepara obtener el estado.

Una cosa a tener en cuenta, asegúrese de que su componente hijo se haya procesado antes de intentar acceder a él ^ _ ^

Como se indicó anteriormente, si usa estas referencias en un nodo DOM en lugar de un componente (por ejemplo <div ref={(divRef) => {this.myDiv = divRef;}} />) this.divRef, devolverá el elemento DOM subyacente en lugar de una instancia de componente.

Más información

Si desea leer más sobre la propiedad de referencia de React, consulte esta página de Facebook.

Asegúrese de leer la sección "No usar en exceso las referencias " que dice que no debe usar al niño statepara "hacer que las cosas sucedan".

Espero que esto ayude ^ _ ^

Editar: React.createRef()Método agregado para crear referencias. Se eliminó el código ES5.

Martoid Prime
fuente
55
También ordenado pequeño tidbit, puede llamar a funciones desde this.refs.yourComponentNameHere. Lo he encontrado útil para cambiar el estado a través de funciones. Ejemplo: this.refs.textInputField.clearInput();
Dylan Pierce
77
Cuidado (de los documentos): las referencias son una excelente manera de enviar un mensaje a una instancia secundaria en particular de una manera que sería inconveniente hacerlo mediante la transmisión de Reactive propsy state. Sin embargo, no deberían ser su abstracción para el flujo de datos a través de su aplicación. Por defecto, use el flujo de datos reactivos y guarde refs para casos de uso que son inherentemente no reactivos.
Lynn
2
Solo obtengo el estado que se definió dentro del constructor (no estado actualizado)
Vlado Pandžić
3
El uso de referencias ahora se clasifica como el uso de ' Componentes no controlados ' con la recomendación de usar 'Componentes controlados'
icc97
¿Es posible hacer esto si el componente hijo es un connectedcomponente Redux ?
jmancherje
7

Es 2020 y muchos de ustedes vendrán aquí buscando una solución similar pero con ganchos (¡son geniales!) Y con los últimos enfoques en términos de limpieza y sintaxis del código.

Entonces, como habían dicho las respuestas anteriores, el mejor enfoque para este tipo de problema es mantener el estado fuera del componente secundario fieldEditor. Podrías hacerlo de múltiples maneras.

El más "complejo" es el contexto global (estado) al que tanto padres como hijos pueden acceder y modificar. Es una gran solución cuando los componentes están muy arraigados en la jerarquía del árbol y, por lo tanto, es costoso enviar accesorios en cada nivel.

En este caso, creo que no vale la pena, y un enfoque más simple nos traerá los resultados que queremos, simplemente usando los poderosos React.useState().

Enfoque con el gancho React.useState (), mucho más simple que con los componentes de la Clase

Como se dijo, trataremos los cambios y almacenaremos los datos de nuestro componente hijo fieldEditoren nuestro padre fieldForm. Para hacerlo, enviaremos una referencia a la función que se ocupará y aplicará los cambios al fieldFormestado, puede hacerlo con:

function FieldForm({ fields }) {
  const [fieldsValues, setFieldsValues] = React.useState({});
  const handleChange = (event, fieldId) => {
    let newFields = { ...fieldsValues };
    newFields[fieldId] = event.target.value;

    setFieldsValues(newFields);
  };

  return (
    <div>
      {fields.map(field => (
        <FieldEditor
          key={field}
          id={field}
          handleChange={handleChange}
          value={fieldsValues[field]}
        />
      ))}
      <div>{JSON.stringify(fieldsValues)}</div>
    </div>
  );
}

Tenga en cuenta que React.useState({})devolverá una matriz con la posición 0 como valor especificado en la llamada (objeto vacío en este caso) y la posición 1 como referencia a la función que modifica el valor.

Ahora con el componente hijo, FieldEditorni siquiera necesita crear una función con una declaración de retorno, ¡una constante lean con una función de flecha servirá!

const FieldEditor = ({ id, value, handleChange }) => (
  <div className="field-editor">
    <input onChange={event => handleChange(event, id)} value={value} />
  </div>
);

Aaaa y hemos terminado, nada más, con estos dos componentes funcionales de limo tenemos nuestro objetivo final "acceder" a nuestro FieldEditorvalor hijo y mostrarlo en nuestros padres.

Puede verificar la respuesta aceptada de hace 5 años y ver cómo Hooks hizo que React code sea más ágil (¡por mucho!).

Espero que mi respuesta te ayude a aprender y comprender más sobre Hooks, y si quieres ver un ejemplo de trabajo aquí está .

Josep Vidal
fuente
4

Ahora puede acceder al estado de InputField que es el hijo de FormEditor.

Básicamente, cada vez que hay un cambio en el estado del campo de entrada (secundario), obtenemos el valor del objeto de evento y luego pasamos este valor al Principal donde se establece el estado en el Principal.

Al hacer clic en el botón, solo estamos imprimiendo el estado de los campos de entrada.

El punto clave aquí es que estamos usando los accesorios para obtener el id / valor del campo de entrada y también para llamar a las funciones que se establecen como atributos en el campo de entrada mientras generamos los campos de entrada secundarios reutilizables.

class InputField extends React.Component{
  handleChange = (event)=> {
    const val = event.target.value;
    this.props.onChange(this.props.id , val);
  }

  render() {
    return(
      <div>
        <input type="text" onChange={this.handleChange} value={this.props.value}/>
        <br/><br/>
      </div>
    );
  }
}       


class FormEditorParent extends React.Component {
  state = {};
  handleFieldChange = (inputFieldId , inputFieldValue) => {
    this.setState({[inputFieldId]:inputFieldValue});
  }
  //on Button click simply get the state of the input field
  handleClick = ()=>{
    console.log(JSON.stringify(this.state));
  }

  render() {
    const fields = this.props.fields.map(field => (
      <InputField
        key={field}
        id={field}
        onChange={this.handleFieldChange}
        value={this.state[field]}
      />
    ));

    return (
      <div>
        <div>
          <button onClick={this.handleClick}>Click Me</button>
        </div>
        <div>
          {fields}
        </div>
      </div>
    );
  }
}

const App = () => {
  const fields = ["field1", "field2", "anotherField"];
  return <FormEditorParent fields={fields} />;
};

ReactDOM.render(<App/>, mountNode);
Dhana
fuente
77
No estoy seguro de que pueda votar este. ¿Qué mencionas aquí que aún no ha sido respondido por @ Markus-ipse?
Alexander Bird
3

Como dicen las respuestas anteriores, intente mover el estado a un componente superior y modificar el estado a través de las devoluciones de llamada pasadas a sus hijos.

En caso de que realmente necesite acceder a un estado secundario que se declara como un componente funcional (ganchos), puede declarar una referencia en el componente principal, luego pasarla como un atributo de referencia al elemento secundario, pero debe usar React.forwardRef y luego el gancho useImperativeHandle para declarar una función que puede llamar en el componente padre.

Eche un vistazo al siguiente ejemplo:

const Parent = () => {
    const myRef = useRef();
    return <Child ref={myRef} />;
}

const Child = React.forwardRef((props, ref) => {
    const [myState, setMyState] = useState('This is my state!');
    useImperativeHandle(ref, () => ({getMyState: () => {return myState}}), [myState]);
})

Entonces debería poder obtener myState en el componente primario llamando a: myRef.current.getMyState();

Mauricio Avendaño
fuente