Avoid recalculating variable on every render in React - javascript

I have a component ParentToDataDisplayingComponent that is creating a few lookups to help format data for a child component based on data in a redux store accessed by the parent of ParentToDataDisplayingComponent.
I am getting some lagging on the components rerendering, where the changing state has not affected this.props.dataOne or this.props.dataTwo - the data in these lookups is guaranteed the same as last render, but the data in props is not guaranteed to be the available (loaded from the backend) when the component mounts. mapPropsToDisplayFormat() is only called after all of the data passed in through the props is available.
I would like to declare the lookup variables once, and avoid re-keyBy()ing on every re-render.
Is there a way to do this inside the ParentToDataDisplayingComponent component?
export default class ParentToDataDisplayingComponent extends Component {
...
mapPropsToDisplayFormat() {
const lookupOne = _(this.props.dataOne).keyBy('someAttr').value();
const lookupTwo = _(this.props.dataTwo).keyBy('someAttr').value();
toReturn = this.props.dataThree.map(data =>
... // use those lookups to build returnObject
);
return toReturn;
}
hasAllDataLoaded() {
const allThere = ... // checks if all data in props is available
return allThere //true or false
}
render() {
return (
<div>
<DataDisplayingComponent
data={this.hasAllDataLoaded() ? this.mapPropsToDisplayFormat() : "data loading"}
/>
</div>
);
}
}

Save the result of all data loading to the component's state.
export default class ParentToDataDisplayingComponent extends Component {
constructor(props) {
super(props)
this.state = { data: "data loading" }
}
componentWillReceiveProps(nextProps) {
// you can check if incoming props contains the data you need.
if (!this.state.data.length && nextProps.dataLoaded) {
this.setState({ data: mapPropsToDisplayFormat() })
}
}
...
render() {
return (
<div>
<DataDisplayingComponent
data={this.state.data}
/>
</div>
);
}
}
I think depending on what exactly you're checking for in props to see if your data has finished loading, you may be able to use shouldComponentUpdate to achieve a similar result without saving local state.
export default class ParentToDataDisplayingComponent extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.hasData !== this.props.hasData
}
mapPropsToDisplayFormat() {
...
toReturn = data.props.dataThree
? "data loading"
: this.props.dataThree.map(data => ... )
return toReturn;
}
render() {
return (
<div>
<DataDisplayingComponent
data={this.mapPropsToDisplayFormat()}
/>
</div>
);
}
}

Related

How to pass updated state to a child component on rendering of the page?

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
}

Updating React/Redux State from Window function

I have a scenario where I'm trying to update a React/Redux state from a function that's placed on the Window. The function on the window is unable to access the function that's in the React component. Any idea how to bind that function in this kind of setup? This snippet just has a console log where the Redux call would go.
class MyComponent extends Component {
updateRedux = a => {
console.log(a)
}
componentDidMount() {
window.windowFunction = function(a) {
this.updateRedux(a)
}
}
render() {
return (
<Stuff />
)
}
}
this is not accessible inside your function, you need to bind it.
Try with:
class MyComponent extends Component {
updateRedux = a => {
console.log(a)
}
componentDidMount() {
window.windowFunction = function(a) {
this.updateRedux(a)
}.bind(this)
}
render() {
return (
<Stuff />
)
}
}
if you meant that you want to update Redux state with some action (this is the only way to update Redux state by design), then you need to make this action and its functions available to your Component with connect(mapStateToProps, mapDispatchToProps)(Component)
One of the comments above about converting the windowFunction to an arrow function resolved the issue. Thanks!
class MyComponent extends Component {
updateRedux = a => {
console.log(a)
}
componentDidMount() {
window.windowFunction = a => {
this.updateRedux(a)
}.bind(this)
}
render() {
return (
<Stuff />
)
}
}
What you could do is separate the concerns using a presenter and a connected
component, using react-redux. I am assuming you know of this library, comment
if you need more details.
// Simple "presenter", the getComponentData is used to get the data for the
// redux store.
class MyComponentPresenter extends Component {
// returns data for redux
getComponentData () {}
componentDidMount() {
this.props.updateRedux(this); // update Redux
}
render() {
return (
<Stuff />
)
}
}
// This component has the exact same interface, but comes with a updateRedux
// props which automatically dispatches an action
export const MyComponent = connect(null, {
updateRedux(componentInstance) {
return {
type: "updateRedux"
};
}
});
// in the reducer
//
function reducer (state, action) {
switch (action.type) {
case "updateRedux":
return ...
}
}
No more need for globally available function (which in your example is redefined for each instance of MyComponents which is probably not what you want).

How to not use setState inside render function in React

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

React component not rendering props coming from parent

class CyInfo extends Component {
foo(){
console.log(this.props.id);
return getAttributes(this.props.id)
}
render() {
return ( <Info data = {this.foo()}> </Info>)
}
}
this parent receive "props.id" and pass data value to children which is returned by getAttributes().
export default class Info extends Component {
constructor(props){
super(props)
}
/*componentWillReceiveProps(nextProps){
console.log(nextProps);
*/
render() {
console.log(this.props.data);
return (
<div id="info">{this.props.data}</div>
)
}
}
On child i can see props value on the console and in componentWillReceiveProps also.But array not rendering.
I try the use react-devtool. In react-devtool props seems passes the children but not rendering. Interestingly in react-devtool when i change the some of array's element array is rendering.
What did i do wrong.
EDIT:
[React-Devtool Screenshot][1]
I edited the react-devtool screenshot. Props are seems but component only renders initial value. In screenshot console error is favicon just ignore this
EDIT2:Console prints props array
EDIT 3:
JSON.stringify(this.props.data)
The props are coming from function(getattributes) which is call a method asynchronous and when this props passed the child there are not rendering.
I call async method directly in parent child and set state with callback in componentWillReceiveProps:
componentWillReceiveProps(nextProps){
self = this;
AsyncFunc(nextProps.id ,(error, result) => {
self.setState({data:result})
});
}
and render with
return (<div id="info">
{Array.isArray(this.state.data) && this.state.data.map((data) => {
return <div key={data._id}>{data.class}{data.predicate}{data.yuklem}</div>
})}</div>
)
As foo is a function, you have to pass to child component as:
return ( <Info data = {() => this.foo()}> </Info>)
Also, data is an array, you have to render using .map(), as follows:
export default class Info extends Component {
constructor(props){
super(props)
}
/*componentWillReceiveProps(nextProps){
console.log(nextProps);
*/
render() {
console.log(this.props.data);
return (
<div id="info">{this.props.data.map(( element, index ) => {
console.log(element);
<span key={index}> {element}</span>})}
</div>
)
}
}
As you have mentioned that this.data.props returns an array, and in order to render elements within an array, you need to map over the array elements and also check that the data is an array or not before rendering as initially the value may not be available or not an array
export default class Info extends Component {
constructor(props){
super(props)
}
/*componentWillReceiveProps(nextProps){
console.log(nextProps);
*/
render() {
console.log(this.props.data);
return (
<div id="info">
{this.props.data && Array.isArray(this.props.data) && this.props.data.map((data, index) => {
return <div key={index}>{data}</div>
})}</div>
)
}
}

React does not render recursive reactive components

Given this component :
import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
export default class SubscriptionView extends TrackerReact(Component) {
constructor(props) {
super(props);
let params = props.params || [];
if (!Array.isArray(params)) {
params = [params];
}
this.state = {
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}
componentWillUnmount() {
this.state.subscription.collection.stop();
}
render() {
let loaded = this.state.subscription.collection.ready();
if (!loaded) {
return (
<section className="subscription-view">
<h3>Loading...</h3>
</section>
);
}
return (
<section className="subscription-view">
{ this.props.children }
</section>
);
}
};
And another component :
import SubscriptionView from './SubscriptionView.jsx';
export const Foo = () => (
<SubscriptionView subscription="allFoo">
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
</SubscriptionView>
);
The first Subscription is re-rendered when the data is available, however the second one is rendered only once and nothing more. If I place a console.log(this.props.subscription, ready); inside the render function of SubscriptionView, I see
allFoo false
allFoo true
singleBar false
and that's it.
On the server side, both publish methods are
Meteor.publish('allFoo', function () {
console.log("Subscribing foos");
return Foos.find();
});
Meteor.publish('singleBar', function (id) {
console.log("Subscribing bar", id);
return Bars.find({ _id: id });
});
Both of the publish methods are being called.
Why isn't the second SubscriptionView reactive?
* Solution *
This is based on alexi2's comment :
import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
export default class SubscriptionLoader extends TrackerReact(Component) {
constructor(props) {
super(props);
let params = props.params || [];
if (!Array.isArray(params)) {
params = [params];
}
this.state = {
done: false,
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}
componentWillUnmount() {
this.state.subscription.collection.stop();
}
componentDidUpdate() {
if (!this.state.done) {
this.setState({ done: true });
this.props.onReady && this.props.onReady();
}
}
render() {
let loaded = this.state.subscription.collection.ready();
if (!loaded) {
return (
<div>Loading...</div>
);
}
return null;
}
};
Then, inside the parent component's render method :
<section className="inventory-item-view">
<SubscriptionLoader subscription='singleBar' params={ this.props.id } onReady={ this.setReady.bind(this, 'barReady') } />
<SubscriptionLoader subscription='allFoos' onReady={ this.setReady.bind(this, 'foosReady') } />
{ content }
</section>
Where setReady merely sets the component's state, and content has a value only if this.state.barReady && this.state.foosReady is true.
It works!
Try separating out your SubscriptionView Components like this:
import SubscriptionView from './SubscriptionView.jsx';
export const Foo = () => (
<div>
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
<SubscriptionView subscription="allFoo">
<div>Rendered Again!</div>
</SubscriptionView>
</div>
);
Edit from comments conversation
Not sure if I am on the right track but you could build Foo as a 'smart' component that passes props to each SubscriptionView as required, and then use Foo as a reusable component.
Let's say that what I need to render is FooBarForm, which requires both Foos and Bars to be registered, in that specific use case. How would you do that?
You could create Components Foos and Bars that took props as required and create a parent component FooBarForm that contained those Components and passed the necessary data.
FooBarForm would handle the state and as that changed pass it to the props of its child components.
Now state is being centrally managed by the parent component, and the child components render using props passed from the parent.
The child components would re-render as their props changed depending on whether the state being passed from the parent component had changed.

Categories