J'ai proposé deux solutions, mais aucune d'entre elles ne semble tout à fait juste.
Première solution:
state.input
.onChange
dans P, qui prend en compte un événement et définit state.input
.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?
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)
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.
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.
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.
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.
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.
shouldComponentUpdate(nextProps, nextState)
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} />
}
}
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.