web-dev-qa-db-fra.com

Réagir - la bonne façon de passer l'état d'élément de formulaire à des éléments frères/parents?

  • Supposons que je possède une classe de réaction P, qui rend deux classes enfants, C1 et C2. 
  • C1 contient un champ de saisie. Je ferai référence à ce champ d'entrée comme Foo. 
  • Mon objectif est de laisser C2 réagir aux changements de Foo. 

J'ai proposé deux solutions, mais aucune d'entre elles ne semble tout à fait juste.

Première solution: 

  1. Attribuez à P un état, state.input.
  2. Créez une fonction onChange dans P, qui prend en compte un événement et définit state.input.
  3. Transmettez cette onChange à C1 en tant que props et laissez C1 lier this.props.onChange à la onChange de Foo.

Cela marche. Chaque fois que la valeur de Foo change, il déclenche une setState dans P, de sorte que P aura l'entrée à transmettre à C2.

Mais cela ne semble pas tout à fait correct pour la même raison: je configure l'état d'un élément parent à partir d'un élément enfant. Cela semble trahir le principe de conception de React: flux de données unidirectionnel.
Est-ce ainsi que je suis censé le faire, ou existe-t-il une solution plus naturelle de React?

Deuxième solution:

Il suffit de mettre Foo dans P.

Mais est-ce un principe de conception que je devrais suivre lorsque je structure mon application - en plaçant tous les éléments de formulaire dans la render de la classe de niveau supérieur?

Comme dans mon exemple, si le rendu de C1 est volumineux, je ne souhaite vraiment pas placer l'ensemble render de C1 sur render de P simplement parce que C1 contient un élément de formulaire.

Comment devrais-je le faire?

158
octref

Donc, si je vous ai bien compris, votre première solution vous suggère de conserver l’état dans votre composant racine? Je ne peux pas parler pour les créateurs de React, mais en général, je trouve cette solution appropriée. 

Le maintien de l’état est l’une des raisons (du moins, je pense) de la création de React. Si vous avez déjà implémenté votre propre côté client de modèle d'état pour gérer une interface utilisateur dynamique comportant de nombreux éléments en mouvement interdépendants, vous allez adorer React, car cela allège en grande partie ces problèmes de gestion d'état. 

En conservant l'état plus haut dans la hiérarchie et en le mettant à jour par le biais d'événements, votre flux de données reste assez unidirectionnel, vous ne répondez qu'aux événements du composant Racine, vous n'obtenez pas vraiment les données via une liaison bidirectionnelle, vous dites au composant racine que "hé, quelque chose s'est passé ici, vérifiez les valeurs" ou que vous transmettez l'état de certaines données dans le composant enfant afin de le mettre à jour. Vous avez modifié l'état dans C1 et vous souhaitez que C2 en soit conscient. Ainsi, en mettant à jour l'état dans le composant racine et en effectuant un nouveau rendu, les accessoires de C2 sont maintenant synchronisés, car l'état a été mis à jour dans le composant racine et transmis. .

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)
175
captray

Ayant déjà utilisé React pour créer une application, j'aimerais partager quelques réflexions sur cette question que j'ai posée il y a six mois.

Je vous recommande de lire

Le premier article est extrêmement utile pour comprendre comment structurer votre application React. 

Flux répond à la question pourquoi devriez-vous structurer votre application React de cette façon (par opposition à comment pour le structurer). React ne représente que 50% du système, et avec Flux, vous obtenez une vue d'ensemble et voyez comment ils constituent un système cohérent. 

Retour à la question.

En ce qui concerne ma première solution, il est tout à fait correct de laisser le handler aller dans le sens inverse, car le data continue toujours dans un sens.

Cependant, le fait de laisser un gestionnaire déclencher un setState dans P peut être correct ou faux, selon votre situation.

Si l'application est un simple convertisseur Markdown, C1 étant l'entrée brute et C2 la sortie HTML, vous pouvez laisser C1 déclencher un setState dans P, mais certains pourraient dire que ce n'est pas la méthode recommandée.

Cependant, si l'application est une liste de tâches, C1 étant l'entrée pour la création d'une nouvelle tâche, C2 la liste de tâches en HTML, vous souhaitez probablement utiliser un gestionnaire pour passer de deux niveaux supérieurs à P à la dispatcher, ce qui permet à la store de mettre à jour le data store, qui envoie ensuite les données à P et remplit les vues. Voir cet article de Flux. Voici un exemple: Flux - TodoMVC

De manière générale, je préfère la méthode décrite dans l'exemple de liste de tâches. Moins vous avez d'état dans votre application, mieux c'est. 

31
octref

La première solution, avec en conservant l'état dans le composant parent , est le correct. Cependant, pour les problèmes plus complexes, vous devriez penser à une bibliothèque de gestion state, redux est la bibliothèque la plus populaire utilisée avec react.

4
Nesha Zoric

Je suis surpris qu'il n'y ait pas de réponse avec une solution idiomatique directe de React au moment où j'écris. Alors voici celui (comparez la taille et la complexité aux autres):

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

Je suis en train de définir l'état d'un élément parent à partir d'un élément enfant. Cela semble trahir le principe de conception de React: flux de données unidirectionnel.

Toute contrôlée input (manière idiomatique de travailler avec des formulaires dans React) met à jour l'état parent dans son rappel onChange et ne trahit toujours rien.

Regardez attentivement le composant C1, par exemple. Voyez-vous une différence significative dans la façon dont C1 et le composant input intégré gèrent les changements d'état? Vous ne devriez pas, car il n'y en a pas. Lever l'état et transmettre les paires valeur/onChange est idiomatique pour la réaction brute. Pas l’utilisation de références, comme le suggèrent certaines réponses.

1
gaperton

Vous devriez apprendre les bibliothèques Redux et ReactRedux.Il structurera vos états et accessoires dans un magasin et vous pourrez y accéder ultérieurement dans vos composants.

1
Ramin Taghizada

Cinq ans plus tard, avec l’introduction des React Hooks, il existe maintenant un moyen beaucoup plus élégant de le faire avec use useContext hook.

Vous définissez le contexte dans une portée globale, exportez des variables, des objets et des fonctions dans le composant parent, puis placez les enfants dans l'application dans un contexte fourni et importez tout ce dont vous avez besoin dans les composants enfants. Vous trouverez ci-dessous une preuve de concept.

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

Vous pouvez exécuter cet exemple dans l'éditeur Code Sandbox.

 Edit passing-state-with-context 

0
J. Wrong
  1. La bonne chose à faire est d’avoir l’état dans le composant parent , pour éviter les références et autres.
  2. Un problème est d'éviter mettre à jour constamment tous les enfants lors de la saisie dans un champ
  3. Par conséquent, chaque enfant doit être un composant (comme dans non un PureComponent) et implémenter shouldComponentUpdate(nextProps, nextState)
  4. Ainsi, lors de la saisie dans un champ de formulaire, seul ce champ est mis à jour

Le code ci-dessous utilise les annotations @bound de ES.Suivant babel-plugin-transform-decorators-legacy de BabelJS 6 et les propriétés de classe (l'annotation définit cette valeur sur les fonctions membres de la même manière que 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} />
  }
}
0
Harald Rudell

Avec React> = 16.3, vous pouvez utiliser ref et forwardRef pour accéder au DOM de l'enfant depuis son parent. N'utilisez plus les anciennes méthodes d'arbitrage. 
Voici l'exemple utilisant votre cas:

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

Voir Refs et ForwardRef pour des informations détaillées sur les refs et forwardRef.

0
Lex Soft