Trigger Re-Render of Child component - reactjs

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
})

Related

Add component on button click

I am making a front end application using typescript and react. I have a component A which amongst other html elements has a textbox. I want to add this component A on click of a button. So if the user clicks the button multiple times, i want a new component A to be created on every click. Also I want to be able to store the text data so that I can later fetch it and process it.
I tried to make a list of this component but it gives me an error.
interface State {
componentList?: ComponentA[];
}
export class ComponentList extends React.Component<Props, State> {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }
public onClick(event) {
const componentList = this.state.componentList;
this.setState({
componentList: componentList.concat(<ComponentA key=
{componentList.length} />)
});
}
  public render() {
    return (
      <React.Fragment>
        <button onClick={this.onClick}>Add component</button>
{this.state.componentList.map(function(component, index)
{
return ComponentA
})}
      </React.Fragment>
    );
  }
}
You might want to make two changes in your code.
First initialise your state in the constructor,
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = { componentList: [] }
}
So that react can track that data.
Second thing is, you are returning wrong item from the map in the render function.
Try returning component, which is different copies of <ComponentA ../> that you pushed every time you clicked the button,
public render() {
return (
<React.Fragment>
<button onClick={this.onClick}>Add component</button>
{this.state.componentList.map(function(component, index)
{
return component;
})}
</React.Fragment>
);
}
Keep the component count in the state:
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {
numComponents: 0
}
}
Add a new function which creates an array of the component for rendering later:
clickedComponents = () => {
let componentArray = [];
for (let i=0; i<this.state.numComponents; i++) {
componentArrays.push(<ComponentA />);
}
return componentArray;
}
Increment the component count with your onClick function:
public onClick(event) {
this.setState({numComponents: this.state.numComponents + 1});
}
Render the component array:
public render() {
return (
<React.Fragment>
<button onClick={this.onClick}>Add component</button>
{this.clickedComponents()}
</React.Fragment>
);
}

ReactJS Assigning Value inside a class component method to props

I'm trying to access the index of the selected tab in a React component so as to map it to props as follows:
class AttendanceDetail extends React.Component {
handleSelect(key, props) {
console.log(key)
props.index = key;
}
render(){
const {single_class, courses, attendances} = this.props;
// console.log(this.state);
if(single_class) {
return(
<div className='container content-section'>
// Some irrelevant Code
<Tabs defaultActiveKey={0} onSelect={this.handleSelect} id="uncontrolled-tab-example">
{ courses.map((course, index) => {
return (
<Tab eventKey={index} title={course.course + " year " + course.yearofstudy}>
//Other irrelevant code...
</Tab>
)
})}
</Tabs>
</div>
)
} else {
return (
<div className='container content-section'>
Loading Unit details...
</div>
);
}
}
}
So basically the handleSelect method is what determines the index of the selected tab and logs it to the console. The problem is, I'm tring to map that key (index) to props so as to access it else where but to no avail. Could someone help me out? What am I missing?
You're not supposed to set the component's props, only read. You can use state within the component:
export class Wrapper extends React.Component {
constructor() {
this.state = {
index: 0 //initial state
}
}
handleSelect(index, props) {
this.setState({index})
}
render() {
return (
<span>{this.state.index}</span>
)
}
}
you can read more on the official docs.
if i understood the scenario correctly, you need to log index value of the currently active tab. try using onFocus event handler to get the index value of the currently visible tab and set the state that will be used by handleSelect
constructor(props){
super(props);
this.state = {
index:''
}
}
the handler definition
setIndex = (index) => {
this.setState({index})
}
update handleSelect
handleSelect(index) {
console.log(index)
// call event handler of parent component eg: this.props.getIndex(index);
}
update tabs component handler
<Tabs defaultActiveKey={0} onSelect={() => {this.handleSelect(this.state.index)}} id="uncontrolled-tab-example">
call handler on focus of tab
<Tab
onFocus={() => {this.setIndex(index)}}
eventKey={index}
title={course.course + " year " + course.yearofstudy}>
//Other irrelevant code...
</Tab>

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: Update Component in Array

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.

Get the object clicked from the child component in React

I'd like to trigger the function handleDisplayProduct on click and pass to it the object clicked. So far it calls the function handleDisplayProduct when the list is generated for all the objects but the function is not triggered on the click event.
So how do i bind the event onclick with the Container and passing to it the element clicked?
Container
// Method to retrieve state from Stores
function getAllProductState(){
return {
products: ProductStore.getProducts(),
};
}
export default class ProductAllContainer extends Component {
constructor(props){
super(props);
this.state = getAllProductState();
}
handleDisplayProduct(data){
console.log(data);
// ProductActions.selectProduct(data)
}
render(){
const products = this.state.products;
return(
<div>
{ products.map(function(product,i){
return (
<ProductThumbnail
product = { product }
key = { i }
**onDisplayProduct = { this.handleDisplayProduct(product) }**
/>
)
},this)}
</div>
)
}
}
View
const ProductThumbnail = (props)=>{
return(
<div>
<LinksTo to="/" **onClick={props.onDisplayProduct}**>
<h1>{props.product.headline}</h1>
<img src={props.product.images[0].imagesUrls.entry[1].url} alt="Thumbnail small pic"/>
</LinksTo>
</div>
)
}
You need to bind the event listener to the react class. You can do it this way.
constructor(props){
super(props);
this.state = getAllProductState();
this.handleDisplayProduct = this.handleDisplayProduct.bind(this);
}
or alternatively using es6, you can use an arrow function instead.
handleDisplayProduct = (data) => {
console.log(data);
// ProductActions.selectProduct(data)
}
Note: Class properties are not yet part of current JavaScript standard. So the second example wouldn't work unless you add a babel-plugin-transform-class-properties babel plugin
Edit: Also #ryanjduffy pointed out a crucial mistake in your code. Refer his comment.

Resources