Quelle est la bonne façon de transmettre l'état d'élément de formulaire aux éléments frères / sœurs / parents?

186
  • Supposons que j'ai une classe React P, qui rend deux classes enfants, C1 et C2.
  • C1 contient un champ de saisie. Je ferai référence à ce champ de saisie comme Foo.
  • Mon objectif est de laisser C2 réagir aux changements de Foo.

J'ai trouvé deux solutions, mais aucune d'elles ne semble tout à fait correcte.

Première solution:

  1. Assigner un état P, state.input.
  2. Créez une onChangefonction dans P, qui prend un événement et définit state.input.
  3. Passez ceci onChangeà C1 en tant que a props, et laissez C1 se lier this.props.onChangeau onChangede Foo.

Cela marche. Chaque fois que la valeur de Foo change, il déclenche a setStatedans P, donc P aura l'entrée pour passer à C2.

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

Deuxième solution:

Mettez simplement 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 du formulaire dans la renderclasse de niveau le plus élevé?

Comme dans mon exemple, si j'ai un grand rendu de C1, je ne veux vraiment pas mettre l'ensemble renderde C1 à renderde P simplement parce que C1 a un élément de formulaire.

Comment dois-je le faire?

octref
la source
Je suis sur le point de faire exactement la même chose et, bien que cela fonctionne correctement, j'ai le sentiment que ce n'est qu'un hack géant
Mirko

Réponses:

198

Donc, si je vous comprends bien, votre première solution suggère que vous gardiez l'état dans votre composant racine? Je ne peux pas parler au nom des créateurs de React, mais en général, je trouve que c'est une bonne solution.

Le maintien de l'état est l'une des raisons (du moins je pense) pour lesquelles React a été créé. Si vous avez déjà implémenté votre propre modèle d'état côté client pour gérer une interface utilisateur dynamique qui a beaucoup de pièces mobiles interdépendantes, alors vous adorerez React, car cela atténue beaucoup de ces problèmes de gestion d'état.

En gardant l'état plus haut dans la hiérarchie et en le mettant à jour via des événements, votre flux de données est toujours à peu près unidirectionnel, vous répondez simplement aux événements dans le 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 vous transmettez l'état de certaines données dans le composant enfant afin de mettre à jour l'état. Vous avez changé l'état dans C1 et vous voulez que C2 en soit conscient, donc, en mettant à jour l'état dans le composant racine et en effectuant un nouveau rendu, les accessoires de C2 sont maintenant synchronisés depuis que 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)
capturer
la source
5
Aucun problème. En fait, je suis retourné après avoir écrit cet article et relu une partie de la documentation, et cela semble être en phase avec leur réflexion et leurs meilleures pratiques. React a une documentation vraiment excellente, et chaque fois que je me demande où quelque chose devrait aller, ils l'ont généralement couvert quelque part dans la documentation. Consultez la section sur l'état ici, facebook.github.io/react/docs/…
captray
@captray mais qu'en est-il, si vous C2avez un getInitialStatepour les données et renderqu'il utilise this.state.data?
Dmitry Polushkin
2
@DmitryPolushkin Si je comprends bien votre question, vous voulez transmettre les données de votre composant racine à C2 comme accessoires. Dans C2, ces données seraient définies comme état initial (c'est-à-dire getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}} et vous voudrez implémenter componentWillReceiveProps et appeler this.setState avec les nouveaux accessoires pour mettre à jour le état dans C2. Depuis que j'ai initialement répondu, j'utilise Flux et je vous recommande vivement de le regarder également. Cela rend vos composants plus propres et changera votre façon de penser à l'état.
captray
3
@DmitryPolushkin Je voulais publier ceci pour faire un suivi. facebook.github.io/react/tips/... C'est correct tant que vous savez ce que vous faites et que vous savez que les données vont changer, mais dans de nombreuses situations, vous pouvez probablement déplacer les choses. Il est également important de noter que vous n'avez pas à construire cela sous forme de hiérarchie. Vous pouvez monter C1 et C2 à différents endroits dans le DOM, et ils peuvent tous deux écouter les événements de changement sur certaines données. Je vois beaucoup de gens pousser pour des composants hiérarchiques quand ils n'en ont pas besoin.
captray
5
Le code ci-dessus a 2 bogues - qui impliquent tous deux de ne pas lier "this" dans le bon contexte, j'ai fait les corrections ci-dessus et aussi pour tous ceux qui ont besoin d'une démonstration codepen
Alex H
34

Ayant utilisé React pour créer une application maintenant, 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 vous devez structurer votre application React.

Flux répond à la question de savoir pourquoi devriez-vous structurer votre application React de cette manière (par opposition à la façon de la 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.

Revenons à la question.

Quant à ma première solution, il est tout à fait normal de laisser le gestionnaire aller dans le sens inverse, car les données vont toujours dans un seul sens.

Cependant, le fait de laisser un gestionnaire déclencher un setState dans P peut être correct ou incorrect 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 manière recommandée de le faire.

Cependant, si l'application est une liste de tâches, C1 étant l'entrée pour créer une nouvelle tâche, C2 la liste de tâches en HTML, vous voulez probablement que le gestionnaire monte de deux niveaux par rapport à P - vers le dispatcher, ce qui permet de storemettre à jour le data store, qui envoient ensuite les données à P et remplissent les vues. Voir cet article Flux. Voici un exemple: Flux - TodoMVC

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

octref
la source
7
J'en discute fréquemment dans des présentations sur React et Flux. Le point que j'essaie de souligner est ce que vous avez décrit ci-dessus, à savoir la séparation de l'état d'affichage et de l'état d'application. Il existe des situations dans lesquelles les choses peuvent passer du simple état d'affichage à l'état d'application, en particulier dans les situations où vous enregistrez l'état de l'interface utilisateur (comme les valeurs prédéfinies). Je pense que c'est génial que vous soyez revenu avec vos propres pensées un an plus tard. +1
captray
@captray Alors peut-on dire que redux est plus puissant que de réagir en toute circonstance? Malgré leur courbe d'apprentissage ... (venant de Java)
Bruce Sun
Je ne sais pas ce que tu veux dire. Ils gèrent très bien deux choses différentes et constituent une bonne séparation des préoccupations.
captray
1
Nous avons un projet utilisant redux, et des mois après, il semble que les actions soient utilisées pour tout. C'est comme ce grand mélange de code spaghetti et impossible à comprendre, un désastre total. La gestion de l'État peut être bonne, mais peut-être gravement mal comprise. Est-il sûr de supposer que flux / redux ne devrait être utilisé que pour les parties de l'état auxquelles il faut accéder globalement?
wayofthefuture
1
@wayofthefuture Sans voir votre code, je peux dire que je vois beaucoup de spaghettis React tout comme le bon vieux spaghetti jQuery. Le meilleur conseil que je puisse offrir est d'essayer de suivre SRP. Rendez vos composants aussi simples que possible; composants de rendu stupides si vous le pouvez. Je fais également pression pour des abstractions comme un composant <DataProvider />. Il fait bien une chose. Fournissez des données. Cela devient généralement le composant «racine» et transmet les données en utilisant les enfants (et le clonage) comme accessoires (contrat défini). En fin de compte, essayez d'abord de penser au serveur. Cela rendra votre JS beaucoup plus propre avec de meilleures structures de données.
captray
6

Cinq ans plus tard, avec l'introduction de React Hooks, il existe maintenant une manière beaucoup plus élégante de le faire avec 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 enveloppez les enfants dans l'application dans un contexte fourni et importez tout ce dont vous avez besoin dans les composants enfants. Voici 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.

Modifier l'état de passage avec contexte

J. Faux
la source
5

La première solution, avec le maintien de l'état dans le composant parent , est la bonne . Cependant, pour des problèmes plus complexes, vous devriez penser à une bibliothèque de gestion d'état , redux est la plus utilisée avec react.

Nesha Zoric
la source
D'accord. Ma réponse a été réécrite alors que la plupart écrivaient des choses en "pure React". Avant l'explosion de flux.
captray le
2

Je suis surpris qu'il n'y ait pas de réponses avec une solution React idiomatique simple 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 définis l'état d'un élément parent à partir d'un élément enfant. Cela semble trahir le principe de conception de React: un flux de données unidirectionnel.

Toute méthode contrôléeinput (manière idiomatique de travailler avec les formulaires dans React) met à jour l'état parent dans son onChangerappel et ne trahit toujours rien.

Regardez attentivement le composant C1, par exemple. Voyez-vous une différence significative dans la manière dont le composant C1intégré inputgère les changements d'état? Vous ne devriez pas, car il n'y en a pas. Lever l'état et transmettre des paires valeur / onChange est idiomatique pour Raw React. Pas d'utilisation de références, comme le suggèrent certaines réponses.

gaperton
la source
1
Quelle version de react utilisez-vous? J'obtiens des problèmes de propriété de classe expérimentale et foo n'est pas défini.
Isaac Pak
Ce n'était pas une version de react. Le code du composant était incorrect. Corrigé, essayez maintenant.
gaperton
De toute évidence, vous devez extraire l'état membre de this.state, le retour était manquant dans le rendu et plusieurs composants doivent être enveloppés dans div ou quelque chose. Je ne sais pas comment j'ai manqué ça quand j'ai écrit la réponse originale. Doit avoir été l'erreur d'édition.
gaperton
1
J'aime cette solution. Si quelqu'un veut le bricoler, voici un bac à sable pour vous.
Isaac Pak
2

Réponse plus récente avec un exemple, qui utilise React.useState

Il est recommandé de conserver l'état dans le composant parent. Le parent doit y avoir accès car il le gère sur deux composants enfants. Le déplacer vers l'état global, comme celui géré par Redux, n'est pas recommandé pour la même raison que la variable globale est pire que locale en général en génie logiciel.

Lorsque l'état est dans le composant parent, l'enfant peut le muter si le parent donne l'enfant valueet le onChangegestionnaire dans les accessoires (parfois, il est appelé lien de valeur ou modèle de lien d'état ). Voici comment vous le feriez avec des crochets:


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'ensemble du composant parent sera rendu lors de la modification d'entrée dans l'enfant, ce qui peut ne pas poser de problème si le composant parent est petit / rapide à restituer. Les performances de re-rendu du composant parent peuvent toujours être un problème dans le cas général (par exemple les grands formulaires). Ce problème est résolu dans votre cas (voir ci-dessous).

Le modèle de lien d'état et l' absence de rendu du parent sont plus faciles à implémenter en utilisant la bibliothèque tierce, comme Hookstate - suralimenté React.useStatepour couvrir une variété de cas d'utilisation, y compris le vôtre. (Avertissement: je suis un auteur du projet).

Voici à quoi cela ressemblerait avec Hookstate. Child1changera l'entrée, Child2y réagira. Parentconservera l'état mais ne sera pas restitué lors du changement d'état, seulement Child1et le Child2fera.

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: il y a beaucoup plus d'exemples ici couvrant des scénarios similaires et plus compliqués, y compris des données profondément imbriquées, la validation d'état, l'état global avec setStatehook, etc. Il existe également un exemple d'application complet en ligne , qui utilise le Hookstate et la technique expliquée ci-dessus.

Andrew
la source
1

Vous devriez apprendre la bibliothèque Redux et ReactRedux, elle structurera vos états et accessoires dans un seul magasin et vous pourrez y accéder plus tard dans vos composants.

Ramin Taghizada
la source
1

Avec React> = 16.3, vous pouvez utiliser ref et forwardRef, pour accéder au DOM de l'enfant depuis son parent. N'utilisez plus l'ancienne méthode de référence.
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 Réfs et ForwardRef pour des informations détaillées sur refs et forwardRef.

Lex Soft
la source
0
  1. La bonne chose à faire est d'avoir l' état dans le composant parent , pour éviter ref et autres
  2. Un problème est d'éviter de mettre à jour constamment tous les enfants lors de la saisie dans un champ
  3. Par conséquent, chaque enfant doit être un composant (comme pas un PureComponent) et implémenter shouldComponentUpdate(nextProps, nextState)
  4. De cette façon, lors de la saisie dans un champ de formulaire, seul ce champ est mis à jour

Le code ci-dessous utilise les @boundannotations de ES.Next babel-plugin-transform-decorators-legacy de BabelJS 6 et class-properties (l'annotation définit cette valeur sur les fonctions membres similaires à 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
la source
0

Le concept de transmission de données du parent à l'enfant et vice versa est expliqué.

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
la source
1
Je vous suggère d'informer le propriétaire de la réponse précédente sous forme de commentaire pour ajouter la description que vous avez fournie dans sa réponse. Suivi en renvoyant votre nom par courtoisie.
Thomas Easo
@ThomasEaso Je ne peux pas le trouver ici, je l'ai vérifié. Y a-t-il d'autres suggestions
akshay
cliquez sur le bouton "modifier" à gauche de son icône et modifiez sa réponse et soumettez. il ira à un modérateur et ils feront le nécessaire pour vos précieux commentaires.
Thomas Easo