Comment accéder à l'état de l'enfant dans React?

215

J'ai la structure suivante:

FormEditor- contient plusieurs FieldEditor FieldEditor- modifie un champ du formulaire et enregistre diverses valeurs à ce sujet dans son état

Lorsqu'un bouton est cliqué dans FormEditor, je veux pouvoir collecter des informations sur les champs de tous les FieldEditorcomposants, des informations qui sont dans leur état, et les avoir toutes dans FormEditor.

J'ai envisagé de stocker les informations sur les champs en dehors de FieldEditorl'état et de les mettre à FormEditorla place. Cependant, cela nécessiterait FormEditord'écouter chacun de ses FieldEditorcomposants lorsqu'ils changent et de stocker leurs informations dans son état.

Je ne peux pas simplement accéder à l'état des enfants à la place? Est-ce que c'est idéal?

Moshe Revah
la source
4
"Je ne peux pas simplement accéder à l'état des enfants à la place? Est-ce l'idéal?" Non. L’État est quelque chose d’intérieur et ne doit pas fuir vers l’extérieur. Vous pouvez implémenter des méthodes d'accesseur pour votre composant, mais même cela n'est pas idéal.
Felix Kling
@FelixKling Alors, vous suggérez que le moyen idéal pour la communication de l'enfant à ses parents n'est que les événements?
Moshe Revah
3
Oui, les événements sont à sens unique. Ou avoir un flux de données unidirectionnel comme Flux promeut: facebook.github.io/flux
Felix Kling
1
Si vous ne comptez pas utiliser les FieldEditors séparément, sauvegarder leur état dans un FormEditorbon son. Si tel est le cas, vos FieldEditorinstances seront rendues en fonction de celles propspassées par leur éditeur de formulaire, et non par leur state. Un moyen plus complexe mais flexible serait de créer un sérialiseur qui passe par tous les enfants de conteneur et trouve toutes les FormEditorinstances parmi eux et les sérialise en un objet JSON. L'objet JSON peut être éventuellement imbriqué (plusieurs niveaux) en fonction des niveaux d'imbrication des instances dans l'éditeur de formulaire.
Meglio
4
Je pense que React Docs 'Lifting State Up' est probablement la façon la plus 'Reacty' de le faire
icc97

Réponses:

136

Si vous avez déjà un gestionnaire onChange pour les FieldEditors individuels, je ne vois pas pourquoi vous ne pouvez pas simplement déplacer l'état vers le composant FormEditor et simplement transmettre un rappel à partir de là aux FieldEditors qui mettront à jour l'état parent. Cela me semble être une façon plus réactive de le faire.

Quelque chose dans le sens de cela peut-être:

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 - version pré-crochets:

Markus-ipse
la source
2
C'est utile pour onChange, mais pas autant dans onSubmit.
190290000 Ruble Man
1
@ 190290000RubleMan Je ne sais pas trop ce que vous voulez dire, mais comme les gestionnaires onChange sur les champs individuels s'assurent que vous avez toujours les données de formulaire complètes disponibles dans votre état dans votre composant FormEditor, votre onSubmit inclura simplement quelque chose comme myAPI.SaveStuff(this.state);. Si vous développez un peu plus, je peux peut-être vous donner une meilleure réponse :)
Markus-ipse
Disons que j'ai un formulaire avec 3 champs de type différent, chacun avec sa propre validation. J'aimerais valider chacun d'eux avant de soumettre. Si la validation échoue, chaque champ contenant une erreur doit être renvoyé. Je n'ai pas compris comment faire cela avec la solution que vous proposez, mais il y a peut-être un moyen!
190290000 Rouble Man
1
@ 190290000RubleMan Semble être un cas différent de la question d'origine. Ouvrez une nouvelle question avec plus de détails et peut-être un exemple de code, et je pourrai y jeter un coup d'œil plus tard :)
Markus-ipse
Cette réponse peut vous être utile : elle utilise des hooks d'état, elle indique où l'état doit être créé, comment éviter le rendu de formulaire à chaque frappe, comment faire la validation de champ, etc.
Andrew
189

Avant d'entrer dans les détails sur la façon dont vous pouvez accéder à l'état d'un composant enfant, assurez-vous de lire la réponse de Markus-ipse concernant une meilleure solution pour gérer ce scénario particulier.

Si vous souhaitez en effet accéder à l'état des enfants d'un composant, vous pouvez affecter une propriété appelée refà chaque enfant. Il existe maintenant deux façons d'implémenter des références: Utilisation React.createRef()et références de rappel.

En utilisant React.createRef()

C'est actuellement la manière recommandée d'utiliser les références à partir de React 16.3 (Voir la documentation pour plus d'informations). Si vous utilisez une version antérieure, consultez ci-dessous les références de rappel.

Vous devrez créer une nouvelle référence dans le constructeur de votre composant parent, puis l'affecter à un enfant via l' refattribut.

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

Pour accéder à ce type de référence, vous devrez utiliser:

const currentFieldEditor1 = this.FieldEditor1.current;

Cela renverra une instance du composant monté afin que vous puissiez ensuite l'utiliser currentFieldEditor1.statepour accéder à l'état.

Juste une petite note pour dire que si vous utilisez ces références sur un nœud DOM au lieu d'un composant (par exemple <div ref={this.divRef} />), alors this.divRef.currentretournera l'élément DOM sous-jacent au lieu d'une instance de composant.

Références de rappel

Cette propriété prend une fonction de rappel qui reçoit une référence au composant attaché. Ce rappel est exécuté immédiatement après le montage ou le démontage du composant.

Par exemple:

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

Dans ces exemples, la référence est stockée sur le composant parent. Pour appeler ce composant dans votre code, vous pouvez utiliser:

this.fieldEditor1

puis utilisez this.fieldEditor1.statepour obtenir l'état.

Une chose à noter, assurez-vous que votre composant enfant est restitué avant d'essayer d'y accéder ^ _ ^

Comme ci-dessus, si vous utilisez ces références sur un nœud DOM au lieu d'un composant (par exemple <div ref={(divRef) => {this.myDiv = divRef;}} />), alors this.divRefretournera l'élément DOM sous-jacent au lieu d'une instance de composant.

Plus d'informations

Si vous souhaitez en savoir plus sur la propriété ref de React, consultez cette page sur Facebook.

Assurez-vous de lire la section « Ne pas abuser des références » qui dit que vous ne devez pas utiliser l'enfant statepour «faire bouger les choses».

J'espère que cela aide ^ _ ^

Modifier: Ajout d'une React.createRef()méthode pour créer des références. Suppression du code ES5.

Martoid Prime
la source
5
Petite astuce également, vous pouvez appeler des fonctions depuis this.refs.yourComponentNameHere. Je l'ai trouvé utile pour changer d'état via des fonctions. Exemple: this.refs.textInputField.clearInput();
Dylan Pierce
7
Attention (à partir des documents): Les références sont un excellent moyen d'envoyer un message à une instance enfant particulière d'une manière qui serait gênante à faire via le streaming Reactive propset state. Cependant, ils ne doivent pas être votre abstraction de référence pour le flux de données via votre application. Par défaut, utilisez le flux de données réactif et enregistrez les refs pour les cas d'utilisation qui sont intrinsèquement non réactifs.
Lynn
2
Je ne reçois que l'état qui a été défini à l'intérieur du constructeur (état pas à jour)
Vlado Pandžić
3
L'utilisation de références maintenant est classée comme utilisant des « composants non contrôlés » avec la recommandation d'utiliser des «composants contrôlés»
icc97
Est-il possible de le faire si le composant enfant est un connectedcomposant Redux ?
jmancherje
7

C'est 2020 et beaucoup d'entre vous viendront ici à la recherche d'une solution similaire mais avec des crochets (ils sont super!) Et avec les dernières approches en termes de propreté et de syntaxe du code.

Ainsi que les réponses précédentes l'avaient indiqué, la meilleure approche pour ce type de problème est de maintenir l'état en dehors de la composante enfant fieldEditor. Vous pouvez le faire de plusieurs manières.

Le plus "complexe" concerne le contexte global (état) auquel les parents et les enfants peuvent accéder et modifier. C'est une excellente solution lorsque les composants sont très profonds dans la hiérarchie de l'arborescence et qu'il est donc coûteux d'envoyer des accessoires à chaque niveau.

Dans ce cas, je pense que cela n'en vaut pas la peine, et une approche plus simple nous apportera les résultats que nous voulons, en utilisant simplement les puissants React.useState().

Approche avec le crochet React.useState (), beaucoup plus simple qu'avec les composants Class

Comme nous l'avons dit, nous allons gérer les changements et stocker les données de notre composant enfant fieldEditordans notre parent fieldForm. Pour ce faire, nous enverrons une référence à la fonction qui traitera et appliquera les modifications à l' fieldFormétat, vous pouvez le faire avec:

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

Notez que React.useState({})retournera un tableau avec la position 0 étant la valeur spécifiée lors de l'appel (objet vide dans ce cas), et la position 1 étant la référence à la fonction qui modifie la valeur.

Maintenant, avec le composant enfant FieldEditor, vous n'avez même pas besoin de créer une fonction avec une instruction return, une constante maigre avec une fonction flèche fera l'affaire!

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

Aaaaand nous avons terminé, rien de plus, avec seulement ces deux composants fonctionnels de slime, nous avons notre objectif final "accéder" à notre FieldEditorvaleur enfant et le montrer dans notre parent.

Vous pouvez vérifier la réponse acceptée il y a 5 ans et voir comment Hooks a rendu le code React plus léger (par beaucoup!).

J'espère que ma réponse vous aidera à en apprendre davantage et à mieux comprendre les crochets, et si vous voulez vérifier un exemple de travail ici, c'est ici .

Josep Vidal
la source
4

Vous pouvez maintenant accéder à l'état de InputField qui est l'enfant de FormEditor.

Fondamentalement, chaque fois qu'il y a un changement dans l'état du champ de saisie (enfant), nous obtenons la valeur de l'objet événement, puis transmettons cette valeur au parent où l'état du parent est défini.

Au clic sur le bouton, nous imprimons simplement l'état des champs de saisie.

Le point clé ici est que nous utilisons les accessoires pour obtenir l'id / valeur du champ d'entrée et également pour appeler les fonctions qui sont définies comme attributs sur le champ d'entrée pendant que nous générons les champs d'entrée enfants réutilisables.

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
la source
7
Je ne suis pas sûr de pouvoir voter pour celui-ci. Que mentionnez-vous ici qui n'a pas déjà été répondu par @ Markus-ipse?
Alexander Bird
4

Comme indiqué dans les réponses précédentes, essayez de déplacer l'état vers un composant supérieur et de modifier l'état via des rappels passés à ses enfants.

Si vous avez vraiment besoin d'accéder à un état enfant déclaré en tant que composant fonctionnel (hooks), vous pouvez déclarer une référence dans le composant parent, puis la transmettre en tant qu'attribut ref à l'enfant, mais vous devez utiliser React.forwardRef puis le hook useImperativeHandle pour déclarer une fonction que vous pouvez appeler dans le composant parent.

Jetez un œil à l'exemple suivant:

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

Ensuite, vous devriez pouvoir obtenir myState dans le composant Parent en appelant: myRef.current.getMyState();

Mauricio Avendaño
la source