Reputation: 325
Problem:
I have a collection of Legislators put into a table, the field headers are buttons and have click events attached to them that sort the collection by the field button clicked.
render() {
const legislatorList = this.populateList() // creates an Array of Legislator components
return (
<div className="legislators">
<h2>Legislators</h2>
<table className="table table-bordered">
<thead>
<tr>
<th><button type="button" onClick={ () => this.handleChange("first_name")} >First</button></th>
<th><button type="button" onClick={ () => this.handleChange("last_name")} >Last</button></th>
<th><button type="button" onClick={ () => this.handleChange("state")} >State</button></th>
<th><button type="button" onClick={ () => this.handleChange("party")} >Party</button></th>
</tr>
</thead>
<tbody>
{ legislatorList }
</tbody>
</table>
</div>
);
The click event re-assigns this.state.legislators to the output of the a sort function.
handleChange( value ) {
this.setState( { legislators: this.sort_by( value) })
}
But the table remains the same even though the updated state of state.legislators is correctly sorted.
I think my problem is that the child Legislator components of Legislators is not updating its state. So my question is how do you update the state of child components?
Other Functions of Legislators:
sort_by(sort_by) {
const sorted = this.state.legislators.sort(function(a, b) {
a_val = a[sort_by]
b_val = b[sort_by]
if (a_val < b_val) {
return -1;
}
if (a_val > b_val) {
return 1;
}
return 0;
});
return sorted
}
renderLegislator(legislator) {
return <Legislator legislator={ legislator}/>
}
populateList() {
return this.state.legislators.map((legislator) =>
this.renderLegislator(legislator)
)}
}
Here is the Legislator component:
class Legislator extends React.Component {
constructor(props) {
super();
this.state = {
legislator: props.legislator,
fields: ["first_name", "last_name", "state", "party"]
}
}
render() {
const legislatorRow = this.state.fields.map((field) =>
<LegislatorField value={ this.state.legislator[field] } />
)
return (
<tr key={this.state.legislator.id}>
{ legislatorRow }
</tr>
);
}
}
Upvotes: 2
Views: 4464
Reputation: 556
The state.legislator
in your child component is not being updated because the code:
this.state = {
legislator: props.legislator,
fields: ["first_name", "last_name", "state", "party"]
}
only runs once as the constructor only runs once, too. And the state.legislator
in child component won't automatically update unless you do this.setState()
explicitly inside the child component.
However, there is no need to do in such a way you are doing. Maintaining own state for props
in the child component is absolutely redundant.
You can directly use this.props
inside render
method in child component, and when the state
changes in the parent component, the child will re-render with new props.
class Legislator extends React.Component {
constructor(props) {
super();
this.state = {
fields: ["first_name", "last_name", "state", "party"]
}
}
render() {
const legislatorRow = this.state.fields.map((field) =>
<LegislatorField value={ this.props.legislator[field] } />
)
return (
<tr key={this.state.legislator.id}>
{ legislatorRow }
</tr>
);
}
}
As a bonus, if your this.state.fields
is always constant -- you don't intend to update it later, you do not have to have it as a state either. You can simply make it a class instance field as this.fields = ["first_name", "last_name", "state", "party"]
Upvotes: 5
Reputation: 7469
The main problem is that Array.prototype.sort
mutates the array and returns it, instead of returning a new array sorted.
a = [4,1,2,3];
b = a.sort();
a === b // true , they are the same object
So when you "change the state" in handleChange
you're actually mutating the state assigning the same object reference, making it difficult to React to tell that you've made any changes at all.
The easy way out would be to use Immutable to store the collection of legislators. Using Immutable.List.sort
you will have a new array with the new value, instead of the old array mutated.
Other way out would be to define your component more clearly by having a sortBy
property on the state that contains the key to sort the list by, and updating that instead of the whole collection.
constructor
this.state = {
legislator: props.legislator,
fields: ["first_name", "last_name", "state", "party"],
sortBy: 'first_name'
}
handleChange
handleChange( value ) {
this.setState( { sortBy: value })
}
populateList
populateList() {
return this.state.legislators.map((legislator) =>
this.renderLegislator(legislator)
).sort(function(a, b) {
a_val = a[this.state.sortBy]
b_val = b[this.state.sortBy]
if (a_val < b_val) {
return -1;
}
if (a_val > b_val) {
return 1;
}
return 0;
});
}
Upvotes: 4