React: Update Component in Array - reactjs

I have a React component that contains an array of child components. The parent retrieves data from a service and stores it in state. It passes an item from the data array to each child component via props.
The child component includes functionality that updates a value in its data item. When it does this, it fires an event, passing the updated item back to the parent. The parent creates a new state array, including the updated item.
Simplified code below.
This all works fine, and the update array is processed in the parent's render method. However, the child components are never re-rendered, so the updated property remains at its previous value.
How can I get the relevant child component to display the updated status?
class SearchView extends Component {
pageSize = 20;
constructor(props) {
super(props);
this.state = {
searchTerm: this.props.searchTerm,
results: []
};
}
getResults = (page) => {
const from = (page - 1) * this.pageSize;
searchActions.termSearch(this.state.searchTerm, from, this.pageSize).then(response => {
const results = response.SearchResultViews;
this.setState({
results: results
});
});
}
componentDidMount() {
this.getResults(1);
}
refresh(result){
const results = this.state.results.map(r => {
return (r.Id === result.Id) ? result : r;
});
this.setState({
results: results
});
}
render() {
let items = [];
if (this.state.results.length > 0) {
items = this.state.results.map((result, i) => {
return <SearchItem key={i} result={result} onStatusUpdate={(r) => this.refresh(r)}></SearchItem>;
});
}
return (
<div className="r-search-result">
<Row className='clearfix scroller'>
<div className='r-container-row results'>
{ items }
</div>
</Row>
</div>
);
}
}
class SearchItem extends Component {
constructor(props) {
super(props);
}
updateStatus(newValue) {
resourceActions.updateStatus(newValue);
//Bubble an event to the Parent to refresh the result and view
if (props.onStatusUpdate) {
searchActions.get(props.result.Id).then((result) => {
props.onStatusUpdate(result);
});
}
}
render() {
return (
<a href={this.props.result.link}>
<span className="column icon-column">{this.props.result.imageUrl}</span>
<span className="column title-column">{this.props.result.titleLink}</span>
<span className="column status-column">{this.props.result.status}</span>
<span className="column button-column"><button onClick={() => this.UpdateStatus(5)}></button></span>
</a>
);
}
}
Edit
In my actual (non-simplified) app, the child component transforms the props it has been passed in the ComponentDidMount() method, and it sets values in state; the render method binds the markup against state, not props. After putting a breakpoint in the child's Render() method as suggested by #Vishal in the comments, I can see that the updated data is received by the child, but since the state hasn't been updated, the component doesn't display the updated data.
The question then is, how best to update the component state without causing an infinite render loop?

In the end, I solved the problem by transforming the properties into state for the child component's in componentWillUpdate(), as well as the componentDidMount() method. As illustrated in the code below:
componentDidMount() {
if (this.props.result) {
this.prepareRender();
}
}
componentWillUpdate(nextProps, nextState) {
if (nextProps.result.status!== this.props.result.status) {
this.prepareRender();
}
}
prepareRender() {
//simplified
this.setState({
imageUrl: this.props.result.imageUrl,
titleLink: this.props.result.titleLink,
status: this.props.result.status
});
}
render() {
return (
<a href={this.props.result.link}>
<span className="column icon-column">{this.state.imageUrl}</span>
<span className="column title-column">{this.state.titleLink}</span>
<span className="column status-column">{this.state.status}</span>
<span className="column button-column"><button onClick={() => this.UpdateStatus(5)}></button></span>
</a>
);
}
UPDATE
In React 16.3 the componentWillUpdate() method is deprecated. This solution should use the new getDerivedStateFromProps() lifecycle method, as explained here: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.

Related

Render child component in parent after re-rendering sibling component

I have a parent component housing two children components(AddPersonForm and PeopleList). When I submit a name via the AddPersonForm, I expect it to be rendered in the PeopleList component, but it doesn't.
Here is my AddPersonForm:
class AddPersonForm extends React.Component {
state = {
person: ""
}
handleChange = (e) => this.setState({person: e.target.value});
handleSubmit = (e) => {
if(this.state.person != '') {
this.props.parentMethod(this.state.person);
this.setState({person: ""});
}
e.preventDefault();
}
render() {
return (
<form onSubmit={this. handleSubmit}>
<input type="text" placeholder="Add new contact" onChange={this.handleChange} value={this.state.person} />
<button type="submit">Add</button>
</form>
);
}
My PeopleList component:
class PeopleList extends React.Component {
constructor(props) {
super(props);
const arr = this.props.data;
this.state = {
listItems: arr.map((val, index) => <li key={index}>{val}</li> );
}
}
render() {
return <ul>{this.state.listItems}</ul>;
}
}
Now the parent component, ContactManager:
class ContactManager extends React.Component {
state = {
contacts: this.props.data
}
addPerson = (name) => {
this.setState({contacts: [... this.state.contacts, name]});
render() {
return (
<div>
<AddPersonForm parentMethod={this. addPerson}×/>
<PeopleList data={this.state.contacts} />
</div>
);
Please what I'm I doing wrong, or not doing?
The issue is in your PeopleList component. The state object which renders your list is created in the constructor when the component mounts, but you have no way of updating it when it recieves new values. It will always give you the initial value.
You could introduce a lifecycle method, componentDidUpdate, which would allow you to compare the previous props to the new props when they arrive, and update the state accordingly. I would recommend you not do this for two reasons:
Storing props directly in a components state is not good practice. You are just creating a copy of the state in the component above and that creates opportunities for confusion and stale values when one of them updates. Ideally, each piece of data should live in only one place.
If all PeopleList is doing is rendering your data, then it doesn't need any state at all. It can act as a display component that maps your props in place and doesn't have to worry about updating itself or managing its own data. This would actually make it a good candidate for conversion into a functional component.
class PeopleList extends React.Component {
render() {
return (
<ul>
{this.props.data.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
);
}
}
You are initializing PeopleList with props when its created and mounted but then you are not using new values of props for updating it.
To fix your issue use current value of prop when rendering:
class PeopleList extends React.Component {
render() {
return <ul>{ this.props.data.map((val, index) => <li key={index}>{val}</li>) }</ul>;
}
}

Why won't child components rerender without using getDerivedStateFromProps?

I cannot figure out why when my selectedEvent successfully changes via setState that my child components do not re-render with the updated state. In the child if I use getDerivedStateFromProps() I can get itto work, but I don't think that should be necessary. I'm missing something.
class App extends Component {
constructor(props) {
super(props);
this.state = {
selectedEvent: '',
eventsData: EventsData,
langData: LangData,
}
this.changeStoryEvent = this.changeStoryEvent.bind(this);
};
changeStoryEvent(event) {
let newEvent = event.target.getAttribute('data-event-key');
if(newEvent != null) {
console.log("new event", newEvent)
this.setState({
selectedEvent: newEvent
})
}
}
render() {
console.log("select", this.state.selectedEvent)
return (
<div>
<div className="header">
<Header />
</div>
<div className="sidebar">
<Sidebar
changeEvent={this.changeStoryEvent}
eventsData={this.state.eventsData}
selectedEvent={this.state.selectedEvent}
/>
</div>
<div className="mainbar">
app event = {this.state.selectedEvent} <br />
<Mainbar
selectedEvent={this.state.selectedEvent}
eventsData={this.state.eventsData}
langData={this.state.langData}
/>
</div>
</div>
)
}
}
class Mainbar extends React.Component {
constructor(props) {
super()
this.state = {
selectedEvent: props.selectedEvent,
changeEventType: props.changeEventType,
eventsData: props.eventsData,
langData: props.langData
}
this.changeEventType = this.changeEventType.bind(this)
}
changeEventType(event) {
}
render() {
console.log('mainstate', this.state)
let eventType = '';
if (this.state.selectedEvent !== '') {
eventType = this.state.eventsData[this.state.selectedEvent].type
}
if (eventType === '') {
eventType = 'action'
}
return (
<div className="mainbar">
mainevent = {this.state.selectedEvent}
<StoryEvent
eventType={eventType}
selectedEvent={this.state.selectedEvent}
changeEventType={this.changeEventType}
eventsData={this.state.eventsData}
langData={this.state.langData}
/>
{this.state.langData.events[this.state.selectedEvent]}
</div>
)
}
}
I see the selectedEvent correctly updated in class App, but not in Mainbar. It apparently does not receive the updated state of selectedEvent.
in you child component, you are copying selectedEvent from props to state, then using this.state.selectedEvent in render.
The constructor is only called once, so you won't update the state of you child later on when the prop change.
Use directly this.props.selectedEvent in the render method

React.js child state not re-rendering even after calling setState?

I have an app with one child component that I would like to re-render when setState updates the bookInput in the parent's state. I am using axios to request info from google's book api. For some reason, even though the state is updating, the child is not re-rendering. Please help if you can! Thank you!
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
bookInput: 'ender',
bookSubmitted: 'initial'
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSubmitEmpty = this.handleSubmitEmpty.bind(this);
}
handleChange(e) {
this.setState({bookInput: e.target.value});
console.log(this.state.bookInput);
//this.setState({bookSubmitted: false});
}
handleSubmit(e) {
e.preventDefault();
//this.setState({bookSubmitted: true})
const name = this.state.bookInput;
this.setState({bookInput: name});
console.log(this.state);
this.setState({bookSubmitted: 'userSub'});
}
handleSubmitEmpty(e) {
alert('please enter an item to search for');
e.preventDefault();
}
render() {
return (
<div className="App">
<header className = "App-header">
<h1>Book Search App</h1>
</header>
<form className = "form-style" onSubmit = {this.state.bookInput ? this.handleSubmit: this.handleSubmitEmpty}>
<label>
<input type="text" className = "input-style"
value = {this.state.bookInput} onChange = {this.handleChange}>
</input>
</label>
<button type="submit">search books</button>
</form>
{/* <Book bookInput = {this.state.bookInput}/> */}
{/*this.state.bookSubmitted && <Book bookInput = {this.state.bookInput}/>*/}
{
(this.state.bookSubmitted === 'initial' || this.state.bookSubmitted === 'userSub') &&
<Book bookInput = {this.state.bookInput}/>
}
</div>
);
}
}
export default App;
class Book extends Component {
constructor(props) {
super(props);
this.state = {
//bookInput2: "ender",
bookTitles: [],
bookExample: '',
isLoading: false
}
this.bookClick = this.bookClick.bind(this);
}
bookClick(book) {
console.log(book);
console.log(book.volumeInfo.infoLink);
const bookURL = book.volumeInfo.infoLink;
window.open(bookURL);
}
componentDidMount() {
//this.setState({ isLoading: true });
this.setState({isLoading: true});
axios.get(`https://www.googleapis.com/books/v1/volumes?q=${this.props.bookInput}`)
.then((response) => {
const bookExample1 = response.data.items;
console.log(bookExample1);
this.setState({bookTitles: bookExample1, isLoading: false});
})
.catch((error) => {
console.error('ERROR!', error);
this.setState({isLoading: false});
});
}
render() {
return (
<div>
{ this.state.bookTitles ? (
<div>
<h2>book list</h2>
{<ul className = 'list-style'>
{this.state.isLoading &&
(<div>
loading book list
</div>)
}
{this.state.bookTitles.map(book => (
<li key={book.id}>
<span className = 'book-details book-title' onClick = {() => this.bookClick(book)}> {book.volumeInfo.title}</span>
<br/>
{book.volumeInfo.imageLinks &&
<img src = {book.volumeInfo.imageLinks.thumbnail}/>
}
{ book.volumeInfo.description &&
<span className = 'book-details'>{book.volumeInfo.description}</span>
}
<br/>
<span className = 'book-details'>Categories {book.volumeInfo.categories}</span>
</li>
))}
</ul>}
</div>) :
(<p>sorry, that search did not return anything</p>)}
</div>
);
}
}
May be you are looking for something similar to this?
https://stackblitz.com/edit/react-snoqkt?file=index.js
The above code can be simplified more and organized but it gives you some idea.
Main changes in the code.
Changed Api call from componentDidMount lifecycle event to a new method named getInitialdata which is called in handleSubmit.
getInitialdata(name){
axios.get(`https://www.googleapis.com/books/v1/volumes?q=${name}`)
.then((response) => {
const bookExample1 = response.data.items;
console.log(bookExample1);
this.setState({bookTitles: bookExample1, isLoading: false, bookSubmitted: 'userSub'});
})
.catch((error) => {
console.error('ERROR!', error);
this.setState({isLoading: false, bookSubmitted: 'userSub'});
});
}
Changed the way how Child component is used.
<Book bookTitles={this.state.bookTitles} isLoading={this.state.isLoading}/>
Issue with your code is you are making an API call in your component's didMount method. This lifecycle event will be invoked only when the component is mounted. Not when it is updated.
When you enter some input in your textbox and click on "Search books", componentDidMount event doesnt fire. And this is the reason why API calls are not happening from the second time.
More on the lifecycle events at https://reactjs.org/docs/react-component.html#componentdidmount
I've taken your code and extrapolated it into this sandbox. Just as you said, your parent component state is updating as it should, but the problem is that the child component doesn't change its state.
A state change will always trigger a re-render in React. The only problem is, your child component is managing it's own state, which isn't directly changing. Instead, it's just receiving new props again and again, but not doing anything with them.
If you look at your code for the <Book /> component, you only modify its state on componentDidMount, which only happens once. If you'd like to programmatically make it update, you can do one of two things.
Remove state from the child component, and make it rely entirely on props, so that it stays in sync with the parent
Use the componentDidUpdate lifecycle method (docs) to choose when to change the state of the child (which will trigger the re-render)

How do I limit the user to only selecting one component?

I have the following code that simply constructs blocks for our products and the selected state allows the component to be selected and unselected. How can I figure out which of these components are selected and limit the user to only selecting one at a time. This is ReactJS code
import React from 'react';
export default class singleTile extends React.Component{
constructor(props){
super(props);
this.title = this.props.title;
this.desc = this.props.desc;
this.svg = this.props.svg;
this.id = this.props.id;
this.state = {
selected: false
}
}
selectIndustry = (event) => {
console.log(event.currentTarget.id);
if(this.state.selected === false){
this.setState({
selected:true
})
}
else{
this.setState({
selected:false
})
}
}
render(){
return(
<div id={this.id} onClick={this.selectIndustry}className={this.state.selected ? 'activated': ''}>
<div className="icon-container" >
<div>
{/*?xml version="1.0" encoding="UTF-8"?*/}
{ this.props.svg }
</div>
</div>
<div className="text-container">
<h2>{this.title}</h2>
<span>{this.desc}</span>
</div>
</div>
)
}
}
You need to manage the state of the SingleTile components in the parent component. What i would do is pass two props to the SingleTile components. A onClick prop which accepts a function and a isSelected prop that accepts a boolean. Your parent component would look something like this.
IndustrySelector.js
import React from 'react';
const tileData = [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }];
class IndustrySelector extends Component {
constructor(props) {
super(props);
this.state = { selectedIndustry: null };
}
selectIndustry(id) {
this.setState({ selectedIndustry: id });
}
isIndustrySelected(id) {
return id === this.state.selectedIndustry;
}
render() {
return (
<div>
{tileData.map((data, key) => (
<SingleTile
key={key}
{...data}
onClick={() => this.selectIndustry(data.id)}
isSelected={this.isIndustrySelected(data.id)}
/>
))}
</div>
);
}
}
The way this works is as follows.
1. Triggering the onClick handler
When a user clicks on an element in SingleTile which triggers the function from the onClick prop, this.selectIndustry in the parent component will be called with the id from the SingleTile component.
Please note that in this example, the id is remembered through a
closure. You could also pass the id as an argument to the function of
the onClick prop.
2. Setting the state in the parent component
When this.selectIndustry is called it changes the selectedIndustry key of the parent component state.
3. Updating the isSelected values form the SIngleTile components
React will automatically re-render the SingleTile components when the state of the parent component changes. By calling this.isIndustrySelected with the id of the SingleTile component, we compare the id with the id that we have stored in the state. This will thus only be equal for the SingleTile that has been clicked for the last time.
Can you post your parent component code?
It's not so important, but you can save some time by using this ES6 feature:
constructor(props){
super(props);
const {title, desc, svg, id, state} = this.props;
this.state = {
selected: false
}
}

Trigger Re-Render of Child component

I'm new to React and am running into the same problem a few times. In this particular situation, I'm trying to get an option in a select dropdown to update when I update a text input.
I have a parent, App, with the state attribute "directions", which is an array. This gets passed as a property to a child, GridSelector, which creates the text field and dropdown. When the text field is changed, a function triggers to update the parent state. This in turn causes the GridSelector property to update. However, the dropdown values, which are originally generated from that GridSelector property, do not re-render to reflect the new property value.
I'm trying to figure out the most React-ful way to do this and similar manuevers. In the past, I've set a state in the child component, but I think I've also read that is not proper.
My working site is at amaxalaus.bigriverwebdesign.com
Here's the pertinent code from each file:
App.js
class App extends React.Component {
constructor(props){
super(props);
this.state = {
directions: [],
dataRouteDirections: '/wp-json/wp/v2/directions',
currentDirectionsIndex: 0
}
this.addImageToGrid = this.addImageToGrid.bind(this);
this.changeTitle=this.changeTitle.bind(this);
}
componentDidMount(){
fetch(this.state.dataRouteDirections)
.then(data => data=data.json())
.then(data => this.setState({directions:data}));
}
addImageToGrid(image) {
this.refs.grid.onAddItem(image); //passes image add trigger from parent to child
}
createNewDirections(){
var directions= this.state.directions;
var index = directions.length;
var lastDirections = directions[directions.length-1];
var emptyDirections= {"id":0,"acf":{}};
emptyDirections.acf.grid="[]";
emptyDirections.acf.layout="[]";
emptyDirections.title={};
emptyDirections.title.rendered="New Directions";
if (lastDirections.id!==0 ) { ///checks if last entry is already blank
this.setState({
directions: directions.concat(emptyDirections), //adds empty directions to end and updates currentdirections
currentDirectionsIndex: index
});
}
}
changeTitle(newTitle){
var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
currentDirections.title.rendered = newTitle;
}
render() {
var has_loaded; //has_loaded was added to prevent double rendering during loading of data from WP
this.state.directions.length > 0 ? has_loaded = 1 : has_loaded = 0;
if (has_loaded ) {
/* const currentGrid = this.state.directions;*/
return ( //dummy frame helpful for preventing redirect on form submit
<div>
<div className="fullWidth alignCenter container">
<GridSelector
directions={this.state.directions}
currentDirectionsIndex={this.state.currentDirectionsIndex}
changeTitle={this.changeTitle}
/>
</div>
<Grid ref="grid"
currentGrid={this.state.directions[this.state.currentDirectionsIndex]}
/>
<ImageAdd addImageToGrid={this.addImageToGrid}/>
<div className="fullWidth alignCenter container">
<button onClick={this.createNewDirections.bind(this)}> Create New Directions </button>
</div>
</div>
)
} else {
return(
<div></div>
)
}
}
}
GridSelector.js
class GridSelector extends React.Component {
constructor(props) {
super(props);
var currentDirections = this.props.directions[this.props.currentDirectionsIndex];
this.state = {
currentTitle:currentDirections.title.rendered
}
}
createOption(direction) {
if (direction.title) {
return(
<option key={direction.id}>{direction.title.rendered}</option>
)
} else {
return(
<option></option>
)
}
}
handleChangeEvent(val) {
this.props.changeTitle(val); //triggers parent to update state
}
render() {
return(
<div>
<select name='directions_select'>
{this.props.directions.map(direction => this.createOption(direction))}
</select>
<div className="fullWidth" >
<input
onChange={(e)=>this.handleChangeEvent(e.target.value)}
placeholder={this.state.currentTitle}
id="directionsTitle"
/>
</div>
</div>
)
}
}
You made a very common beginner mistake. In React state should be handled as an immutable object. You're changing the state directly, so there's no way for React to know what has changed. You should use this.setState.
Change:
changeTitle(newTitle){
var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
currentDirections.title.rendered = newTitle;
}
To something like:
changeTitle(newTitle){
this.setState(({directions,currentDirectionsIndex}) => ({
directions: directions.map((direction,index)=>
index===currentDirectionsIndex? ({...direction,title:{rendered:newTitle}}):direction
})

Resources