Qual è il modo giusto per passare lo stato dell'elemento modulo agli elementi fratello / genitore?


186
  • Supponiamo che io abbia una classe React P, che rende due classi figlio, C1 e C2.
  • C1 contiene un campo di input. Mi riferirò a questo campo di input come Foo.
  • Il mio obiettivo è lasciare che C2 reagisca ai cambiamenti di Foo.

Ho escogitato due soluzioni, ma nessuna delle due sembra abbastanza giusta.

Prima soluzione:

  1. Assegnare P uno stato, state.input.
  2. Crea una onChangefunzione in P, che accetta un evento e imposta state.input.
  3. Passalo onChangea C1 come a propse lascia che C1 si leghi this.props.onChangea onChangeFoo.

Questo funziona Ogni volta che il valore di Foo cambia, innesca a setStatein P, quindi P avrà l'input per passare a C2.

Ma non sembra giusto per lo stesso motivo: sto impostando lo stato di un elemento genitore da un elemento figlio. Questo sembra tradire il principio progettuale di React: flusso di dati a direzione singola.
È così che dovrei farlo, o c'è una soluzione più React-natural?

Seconda soluzione:

Metti Foo in P.

Ma è questo un principio progettuale che dovrei seguire quando strutturo la mia app, mettendo tutti gli elementi del modulo nella renderclasse di più alto livello?

Come nel mio esempio, se ho un rendering di grandi dimensioni di C1, in realtà non voglio mettere l'intero renderC1 su renderdi P solo perché C1 ha un elemento form.

Come dovrei farlo?


Sto per fare esattamente la stessa cosa e, nonostante funzioni correttamente, ho la sensazione che sia solo un grande hack
Mirko,

Risposte:


198

Quindi, se ti sto capendo correttamente, la tua prima soluzione sta suggerendo che stai mantenendo lo stato nel tuo componente radice? Non posso parlare per i creatori di React, ma in generale trovo che questa sia una soluzione adeguata.

Il mantenimento dello stato è uno dei motivi (almeno credo) che React sia stato creato. Se hai mai implementato il tuo lato client del modello di stato per gestire un'interfaccia utente dinamica con molti elementi mobili interdipendenti, adorerai React, perché allevia molto questo dolore di gestione dello stato.

Mantenendo lo stato più in alto nella gerarchia e aggiornandolo tramite gli eventi, il flusso di dati è ancora praticamente unidirezionale, stai solo rispondendo agli eventi nel componente Root, non stai davvero ottenendo i dati lì tramite l'associazione bidirezionale, stai dicendo al componente Root che "ehi, è successo qualcosa quaggiù, controlla i valori" o stai passando lo stato di alcuni dati nel componente figlio per aggiornare lo stato. Hai cambiato lo stato in C1 e vuoi che C2 ne sia consapevole, quindi, aggiornando lo stato nel componente Root e ri-rendering, gli oggetti di scena di C2 sono ora sincronizzati poiché lo stato è stato aggiornato nel componente Root e passato .

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
Nessun problema. In realtà sono tornato dopo aver scritto questo post e ho riletto parte della documentazione, e sembra essere in linea con il loro pensiero e le migliori pratiche. React ha una documentazione davvero eccellente e ogni volta che finisco per chiedermi dove dovrebbe andare qualcosa, in genere la coprono da qualche parte nei documenti.
Dai

@captray ma che dire, se C2hai un getInitialStateper i dati e all'interno di renderesso utilizza this.state.data?
Dmitry Polushkin,

2
@DmitryPolushkin Se capisco correttamente la tua domanda, vuoi passare i dati dal tuo Root Component a C2 come oggetti di scena. In C2, quei dati verrebbero impostati come stato iniziale (cioè getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}} e vorrai implementare componentWillReceiveProps e chiamare this.setState con i nuovi oggetti di scena per aggiornare il stato in C2. Da quando ho inizialmente risposto, sto usando Flux e consiglio vivamente di guardarlo anche. Rende i componenti più puliti e cambierà il modo in cui pensi allo stato.
captray

3
@DmitryPolushkin Volevo postare questo per follow-up. facebook.github.io/react/tips/… Va bene finché sai cosa stai facendo e sai che i dati cambieranno, ma in molte situazioni puoi probabilmente spostare le cose. È anche importante notare che non è necessario crearlo come gerarchia. È possibile montare C1 e C2 in diversi punti del DOM ed entrambi possono ascoltare gli eventi di modifica su alcuni dati. Vedo molte persone che spingono per componenti gerarchici quando non ne hanno bisogno.
captray

5
Il codice sopra ha 2 bug - entrambi implicano non vincolante "questo" nel giusto contesto, ho apportato le correzioni sopra e anche per chiunque abbia bisogno di una dimostrazione codepen
Alex H,

34

Avendo usato React per creare un'app ora, vorrei condividere alcuni pensieri con questa domanda che ho posto sei mesi fa.

Ti consiglio di leggere

Il primo post è estremamente utile per capire come strutturare la tua app React.

Flux risponde alla domanda sul perché dovresti strutturare la tua app React in questo modo (al contrario di come strutturarla). React è solo il 50% del sistema e con Flux puoi vedere l'intero quadro e vedere come costituiscono un sistema coerente.

Torna alla domanda

Per quanto riguarda la mia prima soluzione, è del tutto OK lasciare che il gestore vada nella direzione opposta, poiché i dati stanno ancora andando in una direzione.

Tuttavia, se consentire a un gestore di attivare un setState in P può essere giusto o sbagliato a seconda della situazione.

Se l'app è un semplice convertitore Markdown, C1 è l'input non elaborato e C2 è l'output HTML, è OK lasciare che C1 attivi un setState in P, ma alcuni potrebbero obiettare che questo non è il modo consigliato per farlo.

Tuttavia, se l'app è un elenco di cose da fare, C1 è l'input per la creazione di un nuovo todo, C2 l'elenco di cose da fare in HTML, probabilmente si desidera che il gestore passi a due livelli in più di P - a dispatcher, che consente storeall'aggiornamento di data store, che quindi invia i dati a P e popola le viste. Vedi l'articolo Flux. Ecco un esempio: Flux - TodoMVC

In generale, preferisco il modo descritto nell'esempio della todo list. Meno stato hai nella tua app, meglio è.


7
Ne discuto spesso nelle presentazioni su React e Flux. Il punto che cerco di sottolineare è quello che hai descritto sopra, e cioè la separazione tra View State e Application State. Ci sono situazioni in cui le cose possono passare da un semplice stato di visualizzazione a uno stato di applicazione, soprattutto in situazioni in cui si sta salvando lo stato dell'interfaccia utente (come i valori preimpostati). Penso sia fantastico che tu sia tornato con i tuoi pensieri un anno dopo. +1
captray

@captray Quindi possiamo dire che il redux è potente che reagire in ogni circostanza? Nonostante la loro curva di apprendimento ... (proveniente da Java)
Bruce Sun,

Non sono sicuro di cosa intendi. Gestiscono molto bene due cose diverse e sono una corretta separazione delle preoccupazioni.
captray,

1
Abbiamo un progetto che utilizza il redux e dopo mesi, sembra che le azioni vengano utilizzate per tutto. È come questo grande mix di spaghetti code e impossibile da capire, disastro totale. La gestione dello stato potrebbe essere buona, ma probabilmente fraintesa in modo grave. È sicuro supporre che il flusso / redux debba essere usato solo per parti di stato a cui è necessario accedere a livello globale?
wayofthefuture,

1
@wayofthefuture Senza vedere il tuo codice, posso dire che vedo molti spaghetti Reagire proprio come il buon ole spaghetti jQuery. Il miglior consiglio che posso offrire è di provare a seguire SRP. Rendi i tuoi componenti il ​​più semplice possibile; componenti stupidi se possibile. Inoltre spingo per le astrazioni come un componente <DataProvider />. Fa bene una cosa. Fornire dati. Questo di solito diventa il componente 'root' e trasmette i dati sfruttando i bambini (e la clonazione) come oggetti di scena (contratto definito). Alla fine, prova a pensare prima sul server. Renderà il tuo JS molto più pulito con strutture dati migliori.
captray

6

Cinque anni dopo, con l'introduzione di React Hooks, esiste ora un modo molto più elegante di farlo con use hookContesto.

Definisci il contesto in un ambito globale, esporti variabili, oggetti e funzioni nel componente padre e quindi avvolgi i bambini nell'app in un contesto fornito e importi tutto ciò che ti serve nei componenti figlio. Di seguito è una prova di concetto.

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

È possibile eseguire questo esempio nell'editor Code Sandbox.

Modifica stato di passaggio con contesto


5

La prima soluzione, mantenendo lo stato nel componente genitore , è quella corretta . Tuttavia, per problemi più complessi, dovresti pensare ad alcune librerie di gestione dello stato , redux è la più popolare usata con reagire.


Concordato. La mia risposta è stata riscritta quando molti scrivevano cose in "puro React". Prima della fluorescenza.
captray

2

Sono sorpreso che non ci siano risposte con una soluzione React idiomatica semplice al momento in cui sto scrivendo. Quindi ecco quello (confronta le dimensioni e la complessità con gli altri):

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

Sto impostando lo stato di un elemento padre da un elemento figlio. Questo sembra tradire il principio progettuale di React: flusso di dati a direzione singola.

Qualsiasi modo controllatoinput (modo idiomatico di lavorare con i moduli in React) aggiorna lo stato principale nel suo onChangecallback e non tradisce ancora nulla.

Guarda attentamente il componente C1, per esempio. Vedi qualche differenza significativa nel modo C1in cui il inputcomponente incorporato gestisce i cambiamenti di stato? Non dovresti, perché non ce n'è. Alzare lo stato e tramandare le coppie value / onChange è idiomatico per React grezzo. Non utilizzo di ref, come suggeriscono alcune risposte.


1
Quale versione di reagisci stai usando? Ricevo problemi di proprietà della classe sperimentale e foo non è definito.
Isaac Pak,

Non si trattava di una versione di reagire. Il codice del componente era sbagliato. Risolto il problema, prova ora.
gaperton,

Ovviamente, devi estrarre il membro di stato da this.state, il ritorno mancava nel rendering e più componenti devono essere racchiusi in div o qualcosa del genere. Non so come mi sono perso quando ho scritto la risposta originale. Deve essere stato l'errore di modifica.
gaperton,

1
Mi piace questa soluzione. Se qualcuno vuole armeggiare, ecco una sandbox per te.
Isaac Pak,

2

Risposta più recente con un esempio, che utilizza React.useState

Mantenere lo stato nel componente padre è il modo consigliato. Il genitore deve avere accesso ad esso in quanto lo gestisce attraverso due componenti figlio. Spostarlo nello stato globale, come quello gestito da Redux, non è raccomandato per lo stesso motivo per cui la variabile globale è peggiore di quella locale in generale nell'ingegneria del software.

Quando lo stato è nel componente genitore, il figlio può mutarlo se il genitore fornisce il figlio valuee il onChangegestore in oggetti di scena (a volte viene chiamato collegamento valore o modello collegamento stato ). Ecco come lo faresti con i ganci:


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

L'intero componente genitore verrà sottoposto nuovamente al rendering in caso di modifica dell'input nel figlio, il che potrebbe non essere un problema se il componente genitore è di dimensioni ridotte / veloce per il nuovo rendering. Le prestazioni di nuovo rendering del componente padre possono comunque rappresentare un problema nel caso generale (ad esempio moduli di grandi dimensioni). Questo è un problema risolto nel tuo caso (vedi sotto).

Il modello di collegamento di stato e nessun nuovo rendering del genitore sono più facili da implementare utilizzando la libreria di terze parti, come Hookstate , sovralimentata React.useStateper coprire una varietà di casi d'uso, incluso il tuo. (Dichiarazione di non responsabilità: sono un autore del progetto).

Ecco come sarebbe con Hookstate. Child1cambierà l'ingresso, Child2reagirà ad esso. Parentmanterrà lo stato ma non eseguirà nuovamente il rendering al cambio di stato, solo Child1e lo Child2farà.

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: qui ci sono molti altri esempi che coprono scenari simili e più complicati, tra cui dati profondamente annidati, convalida dello stato, stato globale con setStatehook, ecc. Esiste anche un'applicazione di esempio online completa , che utilizza Hookstate e la tecnica spiegata sopra.


1

Dovresti imparare la libreria Redux e ReactRedux: strutturerà i tuoi stati e oggetti di scena in un negozio e potrai accedervi in ​​seguito nei tuoi componenti.


1

Con React> = 16.3 puoi usare ref e forwardRef, per ottenere l'accesso al DOM figlio dal suo genitore. Non usare più il vecchio modo di fare riferimento.
Ecco l'esempio usando il tuo 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>
    }
}

Vedere Refs e ForwardRef per informazioni dettagliate su refs e forwardRef.


0
  1. La cosa giusta da fare è avere lo stato nel componente genitore , per evitare ref e cosa no
  2. Un problema è evitare di aggiornare costantemente tutti i bambini quando si digita in un campo
  3. Pertanto, ogni figlio dovrebbe essere un Componente (come non un PureComponent) e implementare shouldComponentUpdate(nextProps, nextState)
  4. In questo modo, quando si digita in un campo modulo, viene aggiornato solo quel campo

Il codice seguente utilizza le @boundannotazioni da ES.Next babel-plugin-transform-decorators-legacy di BabelJS 6 e le proprietà della classe (l'annotazione imposta questo valore su funzioni membro simili a bind):

/*
© 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

Viene spiegato il concetto di trasferimento dei dati da genitore a figlio e 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"));


1
Ti suggerirei di informare il precedente proprietario della risposta come commento per aggiungere la descrizione da te fornita nella sua risposta. Seguito ribattendo il tuo nome per cortesia.
Thomas Easo,

@ThomasEaso Non riesco a trovarlo qui, l'ho verificato. C'è qualche altro suggerimento
akshay,

fai clic sul pulsante "modifica" a sinistra della sua icona e modifica la sua risposta e invia. andrà da un moderatore e faranno il necessario per i tuoi preziosi commenti.
Thomas Easo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.