I have a React component that displays information about an entity. The id of the entity is passed in via a property. The component starts an AJAX call in "componentDidMount" to fetch the entity and updates state when the call completes/fails.
This works fine except that the component does not fetch new data when the entity id changes (via props).
I have tried starting a call in "componentWillReceiveProps" but at that stage the component still has is old properties set. I would have to pass nextProps to the AJAX call method and that doesn't seem right.
What is the best/cleanest way to have a component asynchronously update its state in response to a property change?
I'm new to react as well, so the Flux architecture is a bit intimidating to me. I'm doing it exactly as you said, using componentWillMount to load initial data via AJAX, and then componentWillReceiveProps with nextProps to load new data again if/when props change:
var Table = React.createClass({
getInitialState: function() {
return { data: [] };
},
componentWillMount: function(){
this.dataSource();
},
componentWillReceiveProps: function(nextProps){
this.dataSource(nextProps);
},
dataSource: function(props){
props = props || this.props;
return $.ajax({
type: "get",
dataType: 'json',
url: '/products?page=' + props.page + "&pageSize=" + props.pageSize
}).done(function(result){
this.setState({ data: result });
}.bind(this));
},
render: function() {
return (
<table className="table table-striped table-bordered">
<Head />
<Body data={this.state.data}/>
</table>
);
}
});
The hooks componentWillMount and componentWillReceiveProps has been deprecated since React v16.3.0 (source).
AJAX request should be done at the componentDidMount hook when you need to load data inmediatelly after the component is first rendered (source). When you wish to refresh the data after some props changed, you must use the componentDidUpdate hook.
But you'll have to take hand of another three lifecycle hooks to avoid starting an infinite loop of requests/updates. Assuming you wish to update a posts list based on props.category changes:
state should have two properties, category and currentCategory, set null on the component's constructor;
getDerivedStateFromProps is needed to update state.category from the new props.category;
shouldComponentUpdate is needed to compare both state.category and state.currentCategory to determine if the component should be updated;
getSnapshotBeforeUpdate is needed to determine if componentDidUpdate should make an AJAX request or to change the state.currentCategory value and finish the updating cycle.
Complete code would look like this (source):
import React, { Component, Fragment } from 'react';
import axios from "axios";
class Post extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
category: null,
currentCategory: null
};
this._createMarkup = this._createMarkup.bind();
}
static getDerivedStateFromProps(props, state) {
if (props.category !== state.category) {
return {
category: props.category
};
}
return null;
}
componentDidMount() {
this._fetchData();
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.currentCategory !== nextState.category;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return prevState.currentCategory !== prevState.category;
}
componentDidUpdate(prevProps, prevState, dataDidFetch) {
// dataDidFetch is returned by getSnapshotBeforeUpdate
if (dataDidFetch) {
this.setState({
currentCategory: this.state.category
});
} else {
this._fetchData();
}
}
_fetchData() {
const category = this.state.category;
axios.get(`/some/api/endpoint?category=${category}`).then(posts => {
this.setState({
posts: posts.data
});
});
}
_createMarkup(html) {
return { __html: html };
}
render() {
return (
<Fragment>
{this.state.posts.map(post => (
<article className="post" key={post.id}>
<h2>{post.title.rendered}</h2>
<div dangerouslySetInnerHTML={this._createMarkup( post.content.rendered )} />
<p className="post-link">
{post.resource_link_label}
</p>
</article>
))}
</Fragment>
);
}
}
export default Post;
Related
I'm trying to move from componentWillReceiveProps to getDerivedStateFromProps and in some cases, I was successful but when the case is append the props to the existing state, then things start to not behaving the same way. When a make an update to the component's state, the state changes (and the component did after updated) but still renders the previous state. Something weird happens when using getDerivedStateFromProp instead of componentWillReceiveProps. It seems that method does not handle well with 'internal' changes. In the following example, I have getDerivedStateFromProp on Child and it works, but because is only rendering the props. This behavior was also observed in a simpler example where I didn't have any child components and was just rendering state changes.
The code below shows a child component that is used to print/show the data received by props while uses a delete data handler (to remove data that is stored at Parent from child component interaction). When using getDerivedStateFromProps() I can't access to this.state and the prevState doesn't mean the same since the state is accumulative. And when I remove data from the child component, doesn't update the props of the child (while using componentWillReceiveProps was OK). So, I do not find a way to substitute my UNSAFE_componentWillReceiveProps
componentWillReceiveProps:
UNSAFE_componentWillReceiveProps(nextProps){
this.setState({
data: [...this.state.data,...nextProps.data]
})
}
getDerivedStateFromProps:
static getDerivedStateFromProps(nextProps,state) {
if (!isEqual(nextProps.data, state.data)) {
return {
data: [...state.data, ...nextProps.data]
};
}
return null;
}
The original code that works as intended (before getDerivedStateFromProps on Parent Comp.)
DataConsole - Parent Component:
export class DataConsole extends Component {
// Used for unsubscribing when our components unmount
unsub = null;
static defaultProps = {
data: [],
};
constructor(props) {
super(props);
this.state = {
data: [],
};
this.handleTableRowClick = this.handleTableRowClick.bind(this);
}
UNSAFE_componentWillReceiveProps(nextProps){
this.setState({
data: [...this.state.data,...nextProps.data]
})
}
handleTableRowClick(key) {
console.log(
"handleTable",
key,
this.state.data[key],
this.state.data.length
);
const e = this.state.data.splice(key, 1)
//console.log("remove?", e , this.state.data.length)
this.setState({
undoDataRemove: e
});
}
render() {
return (
<div>
<Container
fluid
style={{ paddingLeft: 0, paddingRight: 0 }}
className="DataContainer"
>
<Row noGutters>
<Col sm={8} className="ConsoleTable">
<div>
<DataViewer
data={this.state.data}
rowClickHandler={this.handleTableRowClick}
/>
</div>
...
DataViewer - Child Component
import isEqual from "react-fast-compare";
...
export class DataViewer extends Component {
static defaultProps = {
data: [],
};
constructor(props){
super(props)
this.state={data: []}
}
componentDidUpdate() {
console.log("DataViewer updated");
}
static getDerivedStateFromProps(nextProps, prevProps) {
console.log(nextProps, prevProps)
if (!isEqual(nextProps.data, prevProps.data)) {
return {
data: nextProps.data
};
}
return null;
}
render() {
return (
<div className={"TableData"}>
<Table responsive="lg" striped borderless hover>
<tbody>
{this.state.data.map((elem, ids) => {
if (!isEmpty(elem)) {
return (
<tr key={ids} onClick={() => this.props.rowClickHandler(ids)}>
<td>{ids + 1}</td>
{Object.keys(elem).map(function (value, idx) {
return (
<td key={idx}>
{value}:{elem[value]}
</td>
);
})}
</tr>
);
} else {
return null;
}
})}
</tbody>
</Table>
</div>
);
}
}
There is a bug in your code that causes your problem, and it is unrelated to getDerivedStateFromProps and UNSAFE_componentWillReceiveProps.
The faulty line is this:
const e = this.state.data.splice(key, 1)
It changes this.state.data without calling setState. Never do that. The only way you are ever allowed to make any changes to this.state is via this.setState or by returning something to be merged into state from getDerivedStateFromProps, not any other way.
In order to more easily cope with changes deep in your state tree, immutable libraries come in handily. immer is currently among the top candidates in this category. Using immer, you can modify state any way you want, as long as you wrap it into a produce call using the pattern this.setState(produce(this.state, newState => { /* change newState here */ })):
import produce from 'immer';
// ...
this.setState(produce(this.state, newState => {
const e = newState.data.splice(key, 1);
newState.undoDataRemove = e;
}));
Hope, this can help.
I am using ReactJs. In the below code I am getting data by making API call when the page loads. And I populate the state property. And pass the state to both grid and list view. And that data I want to display in Grid or List component. But, the state property value is not changed and no value is passed to the child component GridView or ListView when the page is rendered. However the state property value get updated but I think the child component is rendered before it gets updated and that's why no value is passed to the child component. Is there a way to pass the updated sate property value on page load?
import GridView from './gridview/Grid';
import ListView from './listview/List';
export default class Home extends Component{
constructor(props){
super(props);
this.state = {
value: this.props.value,
propertyData: []
}
}
componentDidMount() {
const success = fetch("http://Some api to get the data")
.then(res => res.json())
.then(data => this.setState({propertyData: data}));
}
static getDerivedStateFromProps(props, state){
return {
value: props
}
}
GridOrList() {
if(this.state.value.value) {
return <GridView data={this.state.propertyData}/>
}
else {
return <ListView data={this.state.propertyData}/>
}
}
render() {
return(
<div>
{this.GridOrList()}
</div>
)
}
}
All the components reliant on the state will be re-rendered once the state gets changed. You could use loader while your data from api is being fetched:
export default class Home extends Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value,
propertyData: [],
loading: true // Add loading state
};
}
componentDidMount() {
const success = fetch("http://Some api to get the data")
.then(res => res.json())
.then(data => this.setState({ propertyData: data, loading: false })); // Reset loading state
}
static getDerivedStateFromProps(props, state) {
return {
value: props
};
}
GridOrList() {
if (this.state.value.value) {
return <GridView data={this.state.propertyData} />;
} else {
return <ListView data={this.state.propertyData} />;
}
}
render() {
return this.state.loading ? (
<p>Loading data...</p>
) : (
<div>{this.GridOrList()}</div>
);
}
}
try to add the GridOrList on the render function:
render() {
return(
<div>
{this.state.value.value ? (
<GridView data={this.state.propertyData}/>
):(
<ListView data={this.state.propertyData}/>
)}
</div>
)
}
without this.GridOrList()
this.setState will update the state and execute the render function
Try this one:
render() {
return(
<div>
{this.state.value? ( --> // I think the problem is `this.state.value.value`
<GridView data={this.state.propertyData}/>
<ListView data={this.state.propertyData}/>
):null}
</div>
)
}
As you can see here you're assigning value from props to the value key of your state:
this.state = {
value: this.props.value,
propertyData: []
}
I guess you will access it by this.state.value instead of this.state.value.value
setState() is usually asynchronous, which means that at the time you console.log the state, it's not updated yet. Try putting the log in the callback of the setState() method. It is executed after the state change is complete:setState() takes time to mutate the value and you javascript is asynchronous and hence your console.log() will be executed before the setState mutates the values and hence you see the result.
check if the state is updated or not before rendering lists in the render function.
if(state.data !=null) {
//create objects and render the array of objects in return function
}
I am using ComponentDidMount to call data from my database and render page when data is ready. However, i have noticed the speed of my application has reduced when navigating since i have to wait for the data.
This is happening when i have large data in the database i am retrieving. My question is, is there any way of optimizing this, or i just have to render page before data loads ?
Component.JS
componentDidMount()
{
this.fetchAllItems();
}
fetchAllItems(){
return this.fetchPost().then(([response,json]) => {
console.log('here now ',response);
console.log(localStorage.getItem('user_token'))
if(response.status === 200)
{
}
})
}
fetchPost(){
const URL = 'http://localhost:8000/api/';
return fetch(URL, {method:'GET',headers:new Headers ({
'Accept': 'application/json',
'Content-Type': 'application/json',
})})
.then(response => Promise.all([response, response.json()]));
}
Try to use axios to make call to API asynchronously, after it's done, just update your response data to state. No need to wait your page is finished loading or not, react will render by following changes of state value.
import React from 'react';
import axios from 'axios';
export default class MovieList extends React.Component {
state = {
movies: []
}
componentDidMount() {
axios.get(`http://localhost/movies`)
.then(res => {
const movies = res.data;
this.setState({ movies: movies });
})
}
render() {
const {
movies
} = this.state;
return (
<div>
<ul>
{ movies.map(movie => <li>{movie.name}</li>)}
</ul>
</div>
)
}
}
Have you tried the shouldComponentUpdate lifecycle method? This method accepts nextProps (new or upcoming props) and nextState (new or upcoming State) parameters. You can compare your next props and state (state preferably in your case) to determine if your component should re-render or not. Fewer re-renders equals to better speed and optimization. that means your pages will load faster. The shouldComponentUpdate method returns a boolean to determine if a page should re-render or not. Read more here. Also, Here's an example:
class App extends React.Component {
constructor() {
super();
this.state = {
value: true,
countOfClicks: 0
};
this.pickRandom = this.pickRandom.bind(this);
}
pickRandom() {
this.setState({
value: Math.random() > 0.5, // randomly picks true or false
countOfClicks: this.state.countOfClicks + 1
});
}
// comment out the below to re-render on every click
shouldComponentUpdate(nextProps, nextState) {
return this.state.value != nextState.value;
}
render() {
return (
<div>
shouldComponentUpdate demo
<p><b>{this.state.value.toString()}</b></p>
<p>Count of clicks: <b>{this.state.countOfClicks}</b></p>
<button onClick={this.pickRandom}>
Click to randomly select: true or false
</button>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
In your case all the optimization must be done in the backend.
But if there is something that can be done in React is using Should Component Update as previous comment mentioned.
I have a complete running code, but it have a flaw. It is calling setState() from inside a render().
So, react throws the anti-pattern warning.
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount
My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
checkData: true,
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost !== nextProps.cost) {
this.setState({
checkData: true
});
}
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
if (this.props.cost.length && this.state.checkData) {
const tmp = this.props.cost;
//some calculations
....
....
this.setState({
theData: tmp,
checkData: false
});
}
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
<PieGraph theData={this.state.theData} />
</div>
);
}
}
The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.
handleFormSubmit = (e) => {
this.props.onGpChange(this.state.value);
e.preventdefaults();
}
The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?
Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.
checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.
Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost != nextProps.cost) {
this.setState({
theData: this.props.cost
});
}
}
shouldComponentUpdate(nextProps, nextState){
if(nextProps.cost !== this.props.cost){
return true;
}
return false;
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
{this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
</div>
);
}
}
PS:- The above solution is for version React v15.
You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.
There are other ways!
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render
method, both on the initial mount and on subsequent updates. It should
return an object to update the state, or null to update nothing.
So in your case
...component code
static getDerivedStateFromProps(props,state) {
if (this.props.cost == nextProps.cost) {
// null means no update to state
return null;
}
// return object to update the state
return { theData: this.props.cost };
}
... rest of code
You can also use memoization but in your case it's up to you to decide.
The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps
For example updating a list (searching) after a prop changed
You could go from this:
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
to this:
import memoize from "memoize-one";
class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
Something weird is happening, I've been reading the React docs and they talk about the lifecycle and how you can do somestuff before your component is rendered. I am trying, but everything I try is failing, always the component makes the render first and after calls componenWillMount, ..didMount, etc.. and after the call of those functions, the render happens again.
I need to load the data first in order to fill the state because I don't want initial state to be null, I want it with data since the initial rendering.
I am using Flux and Alt, here is the
action
#createActions(flux)
class GetDealersActions {
constructor () {
this.generateActions('dealerDataSuccess', 'dealerDataFail');
}
getDealers (data) {
const that = this;
that.dispatch();
axios.get(`${API_ENDPOINT}/get-dealers/get-dealers`)
.then(function success (response) {
console.log('success GetDealersActions');
that.actions.dealerDataSuccess({...response.data});
})
}
}
then the store
#createStore(flux)
class GetDealersStore {
constructor () {
this.state = {
dealerData : null,
};
}
#bind(GetDealersActions.dealerDataSuccess)
dealerDataSuccess (data) {
this.setState({
dealerData : data,
});
}
}
and the component
#connectToStores
export default class Dealers extends Component {
static propTypes = {
title : React.PropTypes.func,
}
static contextTypes = {
router : React.PropTypes.func,
}
constructor (props) {
super(props);
this.state = {
modal : false,
dealerData : this.props.dealerData,
}
}
componentWillMount () {
GetDealersActions.getDealers();
this.setState({
dealerData : this.props.dealerData.dealersData,
})
}
static getStores () {
return [ GetDealersStore ];
}
static getPropsFromStores () {
return {
...GetDealersStore.getState(),
}
}
render () {
return (<div>
<div style={Styles.mainCont}>
{!!this.props.dealerData ?
this.props.dealerData.dealersData.map((dealer) => {
return (<div>HERE I AM RENDERING WHAT I NEED</div>);
}) : <p>Loading . . .</p>
}
</div>
</div>
);
}
}
as you can see in the component part I have this
constructor (props) {
super(props);
this.state = {
modal : false,
dealerData : this.props.dealerData,
}
}
componentWillMount () {
GetDealersActions.getDealers();
this.setState({
dealerData : this.props.dealerData.dealersData,
})
}
which is telling me that dealerData is undefined or can not read property of null.
All I need is to know a technique where I can fetch the data before the initial renders occurs. So I can filled out the state and the start working with that data.
React does guarantee that state assignments in componentWillMount will take place before the first render. As you well stated in the comments:
Invoked once, both on the client and server, immediately before the initial rendering occurs. If you call setState within this method, render() will see the updated state and will be executed only once despite the state change.
However, the asynchronous actions requested there will not immediately update your store. Calling GetDealersActions.getDealers(); will issue that the store is updated with new content, but the response will only arrive later in the event queue. This means that this.props.dealersData does not change during the function and setState will attempt to read property "dealersData" of an undefined property. Regardless, the requested content cannot be visible at the first render.
My advice is the same as the one in a similar question. Preventing the component from rendering that content until it becomes available, as you did, is an appropriate thing to do in a program. Alternatively, render a loader while your "dealersData" hasn't arrived.
For solving your particular problem, remove that setState from componentWillMount. All should work well if your parent component is properly listening for changes and propagating them to the children's props.
componentWillMount () {
GetDealersActions.getDealers();
}
The best answer I use to receive data from server and display it
constructor(props){
super(props);
this.state = {
items2 : [{}],
isLoading: true
}
}
componentWillMount (){
axios({
method: 'get',
responseType: 'json',
url: '....',
})
.then(response => {
self.setState({
items2: response ,
isLoading: false
});
console.log("Asmaa Almadhoun *** : " + self.state.items2);
})
.catch(error => {
console.log("Error *** : " + error);
});
})}
render() {
return(
{ this.state.isLoading &&
<i className="fa fa-spinner fa-spin"></i>
}
{ !this.state.isLoading &&
//external component passing Server data to its classes
<TestDynamic items={this.state.items2}/>
}
) }