How to avoid using setState in render method? - javascript

I am using react-apollo to fetch data through <Query /> and <Mutation />.
Thus, I want to setState when I get some data. I am getting the data in the render method.
Like this:
render() {
return (
<Query query={CAN_UPDATE_POST_QUERY} variables={{ id: this.props.router.query.postId }}>
{ payload => {
if(payload.loading) {
<div style={{width: '98%', textAlign: 'center', maxWidth: '1000px', margin: '50px auto'}}>Loading...</div>
}
if(this.isNew()){
return (
<PleaseSignIn>
{ me => (
...something
) }
</PleaseSignIn>
)
} else if (payload.data && payload.data.canUpdatePost) {
// I get payload here. Here's where I want to set new state.
this.canUpdatePost = payload.data.canUpdatePost
this.setState({ canUpdatePost: this.canUpdatePost })
return (
<PleaseSignIn>
{ me => (
...something
) }
</PleaseSignIn>
)
} else {
return (
<div style={{width: '98%', textAlign: 'center', maxWidth: '1000px', margin: '50px auto'}}>You and your mind seems to be lost. 🐡</div>
)
}
} }
</Query>
)
}
Using setState in render gives me this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How do I think in React way? And especially, how do I get my state changed when I get payload from react-apollo?
NEWBIE HERE. Sorry if silly.
Thanks in advance. :-)

In general, you should avoid using setState in your render functions. You should avoid having side affects (such as setting state) in your render function and instead should call other component functions to handle data change in your component.
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser.
See the render() method reference in the react docs: https://reactjs.org/docs/react-component.html#render
You can create a function outside of your render method to fix this problem like so:
YourComponent extends React.Component {
handleCanUpdatePost = (canUpdatePos) => {
this.setState({ canUpdatePost })
}
render() {
// Your render logic here
// Whenever you want to setState do so by
// calling this.handleCanUpdatePost(payload.data.canUpdatePost)
}
}
You should also probably have a check to see if the value of state is going to change before setting it to avoid unnecessary re-renders:
handleCanUpdatePost = (canUpdatePos) => {
this.setState((state) => {
if(canUpdatePos !== state.canUpdatePost) {
return {canUpdatePost: payload.data.canUpdatePost}
}
})
}

Related

React re-render conditional on state change without changing the conditional

I've got a conditional that displays an editor while a certain prop remains true. The thing is, the data with which that editor is rendered with should change every time I select another object with which to populate that editor.
However, because the prop responsible for the conditional rendering doesn't change, even though the data with which the editor is rendered does, it refuses to re-render on state change.
I'm not particularly good at React, so, hopefully someone can explain how I can get around this little hiccup.
Conditional render
{this.state.showEditor ? (<BlockEditor routine={this.state.editorObject} />) : null}
Method that is being called.
handleShowEditor = routine => {
this.setState({ showEditor: true });
this.setState({ editorObject: routine });
};
The editor component
export default class BlockEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
routine: this.props.routine
};
}
render() {
return (
<div>
<Editor
autofocus
holderId="editorjs-container"
onChange={data => this.handleSave(data)}
customTools={{}}
onReady={() => console.log("Start!")}
data={this.props.routine.description}
instanceRef={instance => (this.editorInstance = instance)}
/>
</div>
);
}
}
Is there a reason for setting state separately? Why not
handleShowEditor = routine => {
this.setState({
showEditor: true,
editorObject: routine
});
};
Keep in mind that setState is asynchronous and your implementation could lead to such weird behaviour.
If you are still looking for an answer i have faced the same problem working with the same [Editor.JS][1] :).
This worked for me with functional component:
// on change fires when component re-intialize
onChange={async (e) => {
const newData = await e.saver.save();
setEditorData((prevData) => {
console.log(prevData.blocks);
console.log(newData.blocks);
if (
JSON.stringify(prevData.blocks) === JSON.stringify(newData.blocks)
) {
console.log("no data changed");
return prevData;
} else {
console.log("data changed");
return newData;
}
});
}}
// setting true to re-render when currentPage data change
enableReInitialize={true}
Here we are just checking if data changes assign it to editorData component state and perform re-render else assign prevData as it is which will not cause re-render.
Hope it helps.
Edit:
i am comparing editor data blocks change which is array.
of course you need to perform comparison of blocks more deeply than what i am doing, you can use lodash for example.
[1]: https://github.com/editor-js/awesome-editorjs
As setState is asynchronous you can make another call in its callback.
Try like this
handleShowEditor = routine => {
this.setState({
showEditor: true
}, () =>{
this.setState({
editorObject: routine
)}
});
};

useEffect not being called and not updating state when api is fetched

I'm fetching data from a weather api using useEffect hook and declaring the dependency correctly as well. My state is still not being updated and I get errors in my render function because of that. I've pretty much tried everything from getting rid of the dependency array to declaring multiples in the dependency array. I don't know what's wrong with my function. The API's JSON response is in this format:
{
location: {
name: "Paris",
region: "Ile-de-France",
},
current: {
last_updated_epoch: 1564279222,
last_updated: "2019-07-28 04:00",
temp_c: 16,
temp_f: 60.8,
is_day: 0,
condition: {
text: "Clear",
icon: "//cdn.apixu.com/weather/64x64/night/113.png",
code: 1000
},
wind_mph: 6.9,
wind_kph: 11.2
}
}
and this is what my code looks like:
const Weather = ({ capital }) => {
const [weather, setWeather] = useState(null);
useEffect(() => {
console.log("useEffect called");
const getWeather = async () => {
try {
const res = await axios.get(
`http://api.apixu.com/v1/current.json?key=53d601eb03d1412c9c004840192807&q=${capital}`
);
setWeather(res.data);
} catch (e) {
console.log(e);
}
};
getWeather();
}, [capital]);
console.log(weather);
return (
<Card style={{ width: "18rem", marginTop: "25px" }}>
<Card.Img variant="top" src={weather.current.condition.icon} />
<Card.Header style={{ textAlign: "center", fontSize: "25px" }}>
Weather in {capital}
</Card.Header>
</Card>
)
}
I expect to get to be shown image of the icon but I get this error message in my render function:
TypeError: Cannot read property 'current' of null
Weather
src/components/Weather.js:26
23 |
24 | return (
25 | <Card style={{ width: "18rem", marginTop: "25px" }}>
26 | <Card.Img variant="top" src={weather.current.condition.icon} />
| ^ 27 |
28 | <Card.Header style={{ textAlign: "center", fontSize: "25px" }}>
29 | Weather in {capital}
and my console.log(weather) return null, the original state even though its being called after useEffect() and console.log(useEffect called) does not log at all which mean useEffect is not being called.
The error message gives it away, Cannot read property 'current' of null, the only place where current is called is in weather.current in the src of Card.Img, so we deduce that weather was null during the render.
The reason this happens is because the api call is asynchronus, it doesn't populate the state immediately, so the render happens first and tries to read .current from the initial weather state null.
Solution: in your render method, make sure not to read weather.current while weather is null.
You can for example use {weather && <Card>...</Card} to hide the whole card until the data is loaded and show a loading indicator, or you can use src={weather && weather.current.condition.icon} as a quick workaround.
const Weather = ({capital}) => {
const [weather, setWeather] = useState(null);
useEffect(() => {
console.log("useEffect called");
const getWeather = async () => {
try {
const res = await axios.get(
`http://api.apixu.com/v1/current.json?key=53d601eb03d1412c9c004840192807&q=${capital}`,
);
setWeather(res.data);
} catch (e) {
console.log(e);
}
};
getWeather();
}, [capital]);
console.log(weather);
return (
<Card style={{width: "18rem", marginTop: "25px"}}>
<Card.Img variant="top" src={weather && weather.current.condition.icon} />
<Card.Header style={{textAlign: "center", fontSize: "25px"}}>
Weather in {capital}
</Card.Header>
</Card>
);
};
I had the same puzzling issue one time
You can try adding a key prop on the component when it is created in the parent code
<yourcomponent key="some_unique_value" />
This is because in most cases, when your component is reused, based on the way it is created, it may usually re-render it with some changes instead of creating it again when you reuse it, Hence the useEffect is not going to be called. eg in SwitchRoute, loops, conditionals...
So adding a key will prevent this from happening. If it is in a loop, you need to make sure each element is unique, maybe by including the index i in the key if you can't find any better unique key.
So, I was facing a similar issue, where it seemed like useEffect hook was not getting invoked at all. The concerned code snippet is given below:
Within my functional component, this was the sequence of the appearance of the code:
a variable const facetFields = []; declaration which was supposed to be set by a AJAX call from within useEffect hook
useEffect hook within which the above variable was getting set with AJAX.
useEffect( ()=>{
console.log('FacetTreeView: useEffect entered');
facetFields = getFacetFieldNames(props.tabIndex);
}, []);
JSX code that uses the variable.
return (<MyComponent> { facetFields.map((facetLabel, index) => {
populateFacetInstances(facetLabel) }) } </Component>)
With this code, the console statement inside the hook was not printing at all. Also, I kept getting a undefined error while map'ing the facetFields. So, it turns out that the useHook is called after the component is rendered. So, during the rendering, the variable facetFields is undefined.So, I fixed this by adding this line to my JSX rendering part of code:
facetFields && facetFields.map(.....
Once, I made the above change, the control started going to the hook.So, in summary, if there is any code within JSX that is being set from useEffect hook, then its best to defer the execution of the JSX till hook execution is completed. Although, this is not mandatory, but it will make the JSX code easier to read. This can be achieved in a simple way by using a boolean state flag.
Define the state:
const [readyForRender, setReadyForRender] = React.useState(false);
2. Set state in hook.
useEffect( ()=>{
console.log('FacetTreeView: useEffect entered');
facetFields = getFacetFieldNames(props.tabIndex);
setReadyForRender(true); }, []);
Render JSX conditionally.
if(readyForRender){
return ( { facetFields.map((facetLabel, index) => { populateFacetInstances(facetLabel) }) } );
} else{
return null;
}

this.props.children.map at react native is not working

I want to render a child component from a parent component by passing to it one object from array of objects fetched from an api.
TypeError: this.props.posts.map is not a function
renderPosts() {
return this.props.posts.map(post =>
<HomeCard key={post.id} postData={post} />
);
}
All the component:
class Home extends Component {
componentWillMount() {
this.props.getUserPosts();
}
renderPosts() {
return this.props.posts.map(post =>
<HomeCard key={post.id} postData={post} />
);
}
render() {
return (
<View>
<View style={{ paddingBottom: 55 }}>
<SearchBar />
</View>
<ScrollView>
{this.renderPosts()}
</ScrollView>
</View>
);
}
}
const mapStateToProps = state => {
const posts = state.homePost;
console.log('posts', posts);
return { posts };
};
export default connect(mapStateToProps, { getUserPosts })(Home);
I suspect this is because this.props.posts is undefined (empty or whatever default you have it set to) when Home being mounted. Since you aren't giving us any log outputs, it's hard to tell but this is a very common mistake.
The immediate fix is to give it a default value either where you define your initial state for your reducer or in mapStateToProps. The latter looking something like this (adapting your code):
const mapStateToProps = state => {
const posts = state.homePost || [];
console.log('posts', posts);
return { posts };
};
While this will fix your error, another thing you need to correct is the common misconception that whatever is in componentWillMount will execute prior to mounting. This is not true and is one of the reasons that this lifecycle method (and componentWillReceiveProps and componentWillUpdate) will be deprecated in the future.
Your code here:
componentWillMount() {
this.props.getUserPosts();
}
is asynchronous since you mention fetching this data. getUserPosts will fire but isn't guaranteed to complete before mounting. So while you think this.props.posts will be set to some value before rendering, that is not going to be the case. Hence why you are getting the not a function error message.

"Cannot update during an existing state transition" error in React

I'm trying to do Step 15 of this ReactJS tutorial: React.js Introduction For People Who Know Just Enough jQuery To Get By
The author recommends the following:
overflowAlert: function() {
if (this.remainingCharacters() < 0) {
return (
<div className="alert alert-warning">
<strong>Oops! Too Long:</strong>
</div>
);
} else {
return "";
}
},
render() {
...
{ this.overflowAlert() }
...
}
I tried doing the following (which looks alright to me):
// initialized "warnText" inside "getInitialState"
overflowAlert: function() {
if (this.remainingCharacters() < 0) {
this.setState({ warnText: "Oops! Too Long:" });
} else {
this.setState({ warnText: "" });
}
},
render() {
...
{ this.overflowAlert() }
<div>{this.state.warnText}</div>
...
}
And I received the following error in the console in Chrome Dev Tools:
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.
Here's a JSbin demo. Why won't my solution work and what does this error mean?
Your solution does not work because it doesn't make sense logically. The error you receive may be a bit vague, so let me break it down. The first line states:
Cannot update during an existing state transition (such as within render or another component's constructor).
Whenever a React Component's state is updated, the component is rerendered to the DOM. In this case, there's an error because you are attempting to call overflowAlert inside render, which calls setState. That means you are attempting to update state in render which will in then call render and overflowAlert and update state and call render again, etc. leading to an infinite loop. The error is telling you that you are trying to update state as a consequence of updating state in the first place, leading to a loop. This is why this is not allowed.
Instead, take another approach and remember what you're trying to accomplish. Are you attempting to give a warning to the user when they input text? If that's the case, set overflowAlert as an event handler of an input. That way, state will be updated when an input event happens, and the component will be rerendered.
Make sure you are using proper expression. For example, using:
<View onPress={this.props.navigation.navigate('Page1')} />
is different with
<View onPress={ () => this.props.navigation.navigate('Page1')} />
or
<View onPress={ () => {
this.props.navigation.navigate('Page1')
}} />
The two last above are function expression, the first one is not. Make sure you are passing function object to function expression () => {}
Instead of doing any task related to component in render method do it after the update of component
In this case moving from Splash screen to another screen is done only after the componentDidMount method call.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Button,
Image,
} from 'react-native';
let timeoutid;
export default class Splash extends Component {
static navigationOptions = {
navbarHidden: true,
tabBarHidden: true,
};
constructor(props) {
super(props)
this.state = { navigatenow: false };
}
componentDidMount() {
timeoutid=setTimeout(() => {
this.setState({ navigatenow: true });
}, 5000);
}
componentWillUnmount(){
clearTimeout(timeoutid);
}
componentDidUpdate(){
const { navigate,goBack } = this.props.navigation;
if (this.state.navigatenow == true) {
navigate('Main');
}
}
render() {
//instead of writing this code in render write this code in
componenetDidUdpate method
/* const { navigate,goBack } = this.props.navigation;
if (this.state.navigatenow == true) {
navigate('Main');
}*/
return (
<Image style={{
flex: 1, width: null,
height: null,
resizeMode: 'cover'
}} source={require('./login.png')}>
</Image>
);
}
}
Call the component props at each time as new render activity. Warning occurred while overflow the single render.
instead of
<Item onPress = { props.navigation.toggleDrawer() } />
try like
<Item onPress = {() => props.navigation.toggleDrawer() } />
You can also define the function overflowAlert: function() as a variable like so and it will not be called immediately in render
overflowAlert = ()=>{//.....//}

ReactJS 0.14 - Working with promises

OK, I am advancing my project from having simple data arrays in the component to an API call for the data, and as things always are in life, the component no longer works. The API is working I can see the data results, but it is just not producing them in the render:
// function
showMainTestimonial(clientId) {
let results = [];
let linkButtonNode = null;
TestimonyApi.getAll()
.then(function (data) {
var data = data.data;
if (data.showMore) {
linkButtonNode = (
<div style={{ margin:15, width:'100%', textAlign:'center' }}>
<Link className="button" to={ data.testimonialsLink }>More Testimonials</Link></div>)
}
data.testimonials.map(function (testimony, index) {
let commenter = null;
let category = null;
if (testimony.isMain && results.length === 0) {
if (data.showCommenterName) {
commenter = (
<div style={{ textAlign: 'right', fontSize: 16 }}>
<i>-- { testimony.commenter }</i>
</div>
);
}
if (testimony.category) {
category = (
<div style={{ textAlign: 'right', fontSize: 16 }}>
<i> { testimony.category }</i>
</div>
);
}
results.push(
<div id="TestimonialArea" key={ index }>
<div className="main" id="TestimonialZone">
<div id="TestimonialsFeed" className="NavFeed">
<div className="testspcr"></div>
<article className="lists">
<h3>
"{ testimony.title }"
</h3>
<div className="ReviewText">
"{ testimony.comment }"
</div>
{ commenter }
{ category }
</article>
{ linkButtonNode }
</div>
</div>
</div>
)
console.log('results from function: ' + JSON.stringify(results))
return results;
}
});
}.bind(this))
}
// render
render() {
let clientId = this.props.clientId;
var results = this.showMainTestimonial(clientId);
console.log('render results: ' + results);
return (
<section>
{ results }
</section>
)
}
As you can see the data is there, I am just doing something STUPID.
Any ideas?
Thanks in Advance.
You need to take the result of the promise and put it in state, and then have render be based on state.
class Foo extends React.Component {
constructor(){
super();
this.state = {data: null};
}
fetchTestimonial(clientId) {
TestimonyApi.getAll()
.then((x) => this.setState({data: x}))
}
render(){
if (!this.state.data) return <div>Loading</div>
return (
<div>
{this.state.data.map(f)}
</div>
)
}
}
Note: the arrow function is important, it ensures this is correct in the .then callback. Consider only using arrow functions inside methods because it avoids an entire type of bug.
TestimonyApi.getAll / promises are asynchronous. This is a very common problem for people new to JavaScript and has been extensively discussed:
How do I return the response from an asynchronous call?
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
In the context of React, the data should probably be part of the component's state. Instead of returning results (into the void), update the state instead:
this.setState({results});
This will cause the component to rerender. Inside the render method, access this.state.results instead.
You can start the fetching "automatically" by calling the method after the component was mounted. See componentDidMount in Lifecycle Methods.
FYI, the render method should never really fetch data. It is called after the data has changed (either because the component's props or state changed). Maybe reading Thinking in React will help you get a better understanding of this.
I figured out my problem, and refactoring the code makes it clearer to understand.
Basically as FakeRain said, the arrow function was key, although I guess I don't work well with general prose / high altitude theory. No matter, for anyone else struggling.
When you fetch data, and then plan to map it into a specific html 'fragment', split up the fetching and the mapping into 2 different functions, and remember to:
Use the arrow function in the API fetching; (because of 'this')
Call the mapping function in the API function; (because the API call is asynchronous)
Bind the mapping function;
Pass the results of your mapping to state.
So, illustratively speaking:
componentDidMount() {
this.getAPIData();
}
getAPIData() {
TestimonyApi.getAll()
.then((response) => { <-- don't forget the arrow function
let data = response.data;
// call the mapping function inside the API function
this.showMainTestimonial(data)
})
}
showMainTestimonial(data) {
let results = [];
data.map(function(item) {
// do something with mapped data and push in results
results.push (
// customizing the mapping into whatever results you want
)}.bind(this)) <--- don't forget to bind
// pass your configured html into state now after mapping
this.setState({results});
}

Categories

Resources