web-dev-qa-db-fra.com

Comment définir facilement l'état des composants frères dans React?

J'ai les débuts d'un composant de liste cliquable qui servira à piloter un élément select. Comme vous pouvez le voir ci-dessous, onClick de ListItem, je transmets l’état d’un élément enfant (ListItem dans ce cas) aux parents (SelectableList et CustomSelect). Cela fonctionne bien. Cependant, ce que je voudrais aussi faire est de changer l'état des composants fraternité (les autres ListItems) afin que je puisse basculer entre leurs états sélectionnés lorsque l'on clique sur l'un des ListItems. 

Pour le moment, j'utilise simplement document.querySelectorAll('ul.cs-select li) pour récupérer les éléments et changer la classe en sélectionnée si elle ne correspond pas à l'index de la ListItem cliquée. Cela fonctionne - dans une certaine mesure. Cependant, après quelques clics, l’état du composant n’a pas été mis à jour par React (uniquement par JS côté client) et la situation commence à se détériorer. Ce que je voudrais faire, c'est changer le this.state.isSelected des éléments de la liste des frères et soeurs et utiliser cet état pour actualiser le composant SelectableList. Quelqu'un pourrait-il offrir une meilleure alternative à ce que j'ai écrit ci-dessous?

var React = require('react');
var SelectBox = require('./select-box');

var ListItem = React.createClass({
    getInitialState: function() {
        return {
            isSelected: false
        };
    },

    toggleSelected: function () {
        if (this.state.isSelected == true) {
            this.setState({
                isSelected: false
            })
        } else {
            this.setState({
                isSelected: true
            })
        }
    },

    handleClick: function(listItem) {
        this.toggleSelected();
        this.props.onListItemChange(listItem.props.value);

        var unboundForEach = Array.prototype.forEach,
            forEach = Function.prototype.call.bind(unboundForEach);

        forEach(document.querySelectorAll('ul.cs-select li'), function (el) {

            // below is trying to 
            // make sure that when a user clicks on a list
            // item in the SelectableList, then all the *other*
            // list items get class="selected" removed.
            // this works for the first time that you move through the 
            // list clicking the other items, but then, on the second
            // pass through, starts to fail, requiring *two clicks* before the
            // list item is selected again.
            // maybe there's a better more "reactive" method of doing this?

            if (el.dataset.index != listItem.props.index && el.classList.contains('selected') ) {
                el.classList.remove('selected');
            }
        });
    },

    render: function() {
        return (
            <li ref={"listSel"+this.props.key}
                data-value={this.props.value}
                data-index={this.props.index}
                className={this.state.isSelected == true ? 'selected' : '' } 
                onClick={this.handleClick.bind(null, this)}>
                {this.props.content}
            </li>
        );
    }
});

var SelectableList = React.createClass({

    render: function() {

        var listItems = this.props.options.map(function(opt, index) {
            return <ListItem key={index} index={index} 
                        value={opt.value} content={opt.label}
                        onListItemChange={this.props.onListItemChange.bind(null, index)} />;
        }, this);

        return <ul className="cs-select">{ listItems }</ul>;
    }

})

var CustomSelect = React.createClass({

    getInitialState: function () {
        return {
            selectedOption: ''
        }
    },

    handleListItemChange: function(listIndex, listItem) {
        this.setState({
            selectedOption: listItem.props.value
        })
    },

    render: function () {

        var options = [{value:"One", label: "One"},{value:"Two", label: "Two"},{value:"Three", label: "Three"}];

        return (
            <div className="group">
                <div className="cs-select">
                    <SelectableList options={options} 
                        onListItemChange={this.handleListItemChange} />
                    <SelectBox className="cs-select" 
                        initialValue={this.state.selectedOption} 
                        fieldName="custom-select" options={options}/>
                </div>
            </div>
        )
    } 
})

module.exports = CustomSelect;
22
The Pied Pipes

Le composant parent doit transmettre un rappel aux enfants, et chaque enfant déclenche ce rappel lorsque son état change. Vous pouvez en fait contenir tout l'état du parent, l'utiliser comme un seul point de vérité et transmettre la valeur "sélectionnée" à chaque enfant en tant qu'accessoire.

Dans ce cas, l'enfant pourrait ressembler à ceci:

var Child = React.createClass({
    onToggle: function() {
        this.props.onToggle(this.props.id, !this.props.selected);
    },

    render: function() {
        return <button onClick={this.onToggle}>Toggle {this.props.label} - {this.props.selected ? 'Selected!' : ''}!</button>;
    }
});

Il n'a pas d'état, il déclenche simplement un rappel onToggle lorsque l'utilisateur clique dessus. Le parent ressemblerait à ceci:

var Parent = React.createClass({
    getInitialState: function() {
        return {
            selections: []
        };
    },
    onChildToggle: function(id, selected) {
        var selections = this.state.selections;

        selections[id] = selected;

        this.setState({
            selections: selections
        });
    },

    buildChildren: function(dataItem) {
        return <Child
            id={dataItem.id}
            label={dataItem.label}
            selected={this.state.selections[dataItem.id]}
            onToggle={this.onChildToggle} />
    },

    render: function() {
        return <div>{this.props.data.map(this.buildChildren)}</div>
    }
});

Il contient un tableau de sélections dans l'état et lorsqu'il traite le rappel d'un enfant, il utilise setState pour restituer le rendu des enfants en transmettant son état dans la propriété selected à chaque enfant.

Vous pouvez voir un exemple de travail de ceci ici:

https://jsfiddle.net/fth25erj/

22
Colin Ramsay

Une autre stratégie de communication entre frères consiste à utiliser un modèle d'observateur.

Le modèle d'observateur est un modèle de conception logicielle dans lequel un objet peut envoyer des messages à plusieurs autres objets.

Aucune relation de fratrie ou parent-enfant n'est requise pour utiliser cette stratégie.

Dans le contexte de React, cela signifie que certains composants s'abonnent pour recevoir des messages particuliers et que d'autres composants publient des messages à l'intention de ces abonnés.

Les composants s'abonnent généralement dans la méthode ComponentDidMount et se désabonnent dans la méthode ComponentWillUnmount.

Voici 4 bibliothèques qui implémentent le modèle Observer. Les différences entre eux sont subtiles - EventEmitter est le plus populaire.

  • PubSubJS : "une bibliothèque de publication/abonnement basée sur un sujet écrite en JavaScript."
  • EventEmitter : "JavaScript eventé pour le navigateur." Il s’agit en fait d’une implémentation d’une bibliothèque qui fait déjà partie du coeur de nodejs, mais pour le navigateur.
  • MicroEvent.js : "microlibothèque émettrice d'événement - 20 lignes - pour noeud et navigateur" 
  • mobx : "Gestion d'état simple et évolutive."

Tiré de: 8 stratégies sans flux pour la communication avec le composant React qui est également une excellente lecture en général.

1
Mrchief

Le code suivant m'aide à configurer la communication entre deux frères et soeurs. La configuration est effectuée dans leur parent lors des appels render () et composantDidMount ().

class App extends React.Component<IAppProps, IAppState> {
    private _navigationPanel: NavigationPanel;
    private _mapPanel: MapPanel;

    constructor() {
        super();
        this.state = {};
    }

    // `componentDidMount()` is called by ReactJS after `render()`
    componentDidMount() {
        // Pass _mapPanel to _navigationPanel
        // It will allow _navigationPanel to call _mapPanel directly
        this._navigationPanel.setMapPanel(this._mapPanel);
    }

    render() {
        return (
            <div id="appDiv" style={divStyle}>
                // `ref=` helps to get reference to a child during rendering
                <NavigationPanel ref={(child) => { this._navigationPanel = child; }} />
                <MapPanel ref={(child) => { this._mapPanel = child; }} />
            </div>
        );
    }
}
0
Sergei Zinovyev