Thursday, May 9, 2024
 Popular · Latest · Hot · Upcoming
136
rated 0 times [  138] [ 2]  / answers: 1 / hits: 75238  / 8 Years ago, mon, march 21, 2016, 12:00:00

I'm new to React, and I'd like to ask a strategy question about how best to accomplish a task where data must be communicated between sibling components.



First, I'll describe the task:



Say I have multiple <select> components that are children of a single parent that passes down the select boxes dynamically, composed from an array. Each box has exactly the same available options in its initial state, but once a user selects a particular option in one box, it must be disabled as an option in all other boxes until it is released.



Here's an example of the same in (silly) code. (I'm using react-select as a shorthand for creating the select boxes.)



In this example, I need to disable (ie, set disabled: true) the options for It's my favorite and It's my least favorite when a user selects them in one select box (and release them if a user de-selects them).





var React = require('react');
var Select = require('react-select');



var AnForm = React.createClass({

render: function(){


// this.props.fruits is an array passed in that looks like:
// ['apples', 'bananas', 'cherries','watermelon','oranges']
var selects = this.props.fruits.map(function(fruit, i) {

var options = [
{ value: 'first', label: 'It's my favorite', disabled: false },
{ value: 'second', label: 'I'm OK with it', disabled: false },
{ value: 'third', label: 'It's my least favorite', disabled: false }
];


return (
<Child fruit={fruit} key={i} options={options} />
);
});


return (
<div id=myFormThingy>
{fruitSelects}
</div>
)
}

});


var AnChild = React.createClass({

getInitialState: function() {
return {
value:'',
options: this.props.options
};
},

render: function(){

function changeValue(value){
this.setState({value:value});
}


return (
<label for={this.props.fruit}>{this.props.fruit}</label>
<Select
name={this.props.fruit}
value={this.state.value}
options={this.state.options}
onChange={changeValue.bind(this)}
placeholder=Choose one
/>
)
}
});





Is updating the child options best accomplished by passing data back up to the parent through a callback? Should I use refs to access the child components in that callback? Does a redux reducer help?



I apologize for the general nature of the question, but I'm not finding a lot of direction on how to deal with these sibling-to-sibling component interactions in a unidirectional way.



Thanks for any help.


More From » reactjs

 Answers
4

TLDR: Yes, you should use a props-from-top-to-bottom and change-handlers-from-bottom-to-top approach. But this can get unwieldy in a larger application, so you can use design patterns like Flux or Redux to reduce your complexity.



Simple React approach



React components receive their inputs as props; and they communicate their output by calling functions that were passed to them as props. A canonical example:



<input value={value} onChange={changeHandler}>


You pass the initial value in one prop; and a change handler in another prop.



Who can pass values and change handlers to a component? Only their parent. (Well, there is an exception: you can use the context to share information between components, but that's a more advanced concept, and will be leveraged in the next example.)



So, in any case, it's the parent component of your selects that should manage the input for your selects. Here is an example:



class Example extends React.Component {

constructor(props) {
super(props);
this.state = {
// keep track of what is selected in each select
selected: [ null, null, null ]
};
}

changeValue(index, value) {
// update selected option
this.setState({ selected: this.state.selected.map((v, i) => i === index ? value : v)})
}

getOptionList(index) {
// return a list of options, with anything selected in the other controls disabled
return this.props.options.map(({value, label}) => {
const selectedIndex = this.state.selected.indexOf(value);
const disabled = selectedIndex >= 0 && selectedIndex !== index;
return {value, label, disabled};
});
}

render() {
return (<div>
<Select value={this.state.selected[0]} options={this.getOptionList(0)} onChange={v => this.changeValue(0, v)} />
<Select value={this.state.selected[1]} options={this.getOptionList(1)} onChange={v => this.changeValue(1, v)} />
<Select value={this.state.selected[2]} options={this.getOptionList(2)} onChange={v => this.changeValue(2, v)} />
</div>)
}

}


Redux



The main drawback of the above approach is that you have to pass a lot of information from the top to the bottom; as your application grows, this becomes difficult to manage. React-Redux leverages React's context feature to enable child components to access your Store directly, thus simplifying your architecture.



Example (just some key pieces of your redux application - see the react-redux documentation how to wire these together, e.g. createStore, Provider...):



// reducer.js

// Your Store is made of two reducers:
// 'dropdowns' manages the current state of your three dropdown;
// 'options' manages the list of available options.

const dropdowns = (state = [null, null, null], action = {}) => {
switch (action.type) {
case 'CHANGE_DROPDOWN_VALUE':
return state.map((v, i) => i === action.index ? action.value : v);
default:
return state;
}
};

const options = (state = [], action = {}) => {
// reducer code for option list omitted for sake of simplicity
};

// actionCreators.js

export const changeDropdownValue = (index, value) => ({
type: 'CHANGE_DROPDOWN_VALUE',
index,
value
});

// helpers.js

export const selectOptionsForDropdown = (state, index) => {
return state.options.map(({value, label}) => {
const selectedIndex = state.dropdowns.indexOf(value);
const disabled = selectedIndex >= 0 && selectedIndex !== index;
return {value, label, disabled};
});
};

// components.js

import React from 'react';
import { connect } from 'react-redux';
import { changeDropdownValue } from './actionCreators';
import { selectOptionsForDropdown } from './helpers';
import { Select } from './myOtherComponents';

const mapStateToProps = (state, ownProps) => ({
value: state.dropdowns[ownProps.index],
options: selectOptionsForDropdown(state, ownProps.index)
}};

const mapDispatchToProps = (dispatch, ownProps) => ({
onChange: value => dispatch(changeDropdownValue(ownProps.index, value));
});

const ConnectedSelect = connect(mapStateToProps, mapDispatchToProps)(Select);

export const Example = () => (
<div>
<ConnectedSelect index={0} />
<ConnectedSelect index={1} />
<ConnectedSelect index={2} />
</div>
);


As you can see, the logic in the Redux example is the same as the vanilla React code. But it is not contained in the parent component, but in reducers and helper functions (selectors). An instead of top-down passing of props, React-Redux connects each individual component to the state, resulting in a simpler, more modular, easier-to-maintain code.


[#62851] Saturday, March 19, 2016, 8 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
tayaw

Total Points: 749
Total Questions: 88
Total Answers: 86

Location: Djibouti
Member since Sun, Feb 27, 2022
2 Years ago
tayaw questions
;