Can't set state inside a method that gets called in render - javascript

I know there are a bunch of answers out there for this question, but I feel like none of them were quite what I was looking for. My problem is that I have this application that gets a list of stores within a desired radius. When I click submit, I populate an array with those stores information. What I want to do is be able to store some of that information into an array in my react state. My main issue is that I would like to set state when that information is populated, but this causes an infinite loop because setState calls the render method again. Hopefully this explains my issue a little better, there is some code to go with this below as well. Any help is appreciated, thanks!
stores.js
Class stores extends React.Component{
constructor(props){
this.state = {
StoreInfo: [],
Zip: "",
Radius: ""
}
}
RenderTable(zip, radius){
.... some stuff to generate stores within this location
storeInfo = []
for(var i = 0; i < storeArray.length; i++){
storeInfo.push(storeArray.info);
}
this.setState({StoreInfo: storeInfo});
... generate html to be rendered
}
render(){
var renderPage = (
<div>
{this.RenderTable(this.state.Zip, this.state.Radius)}
</div>
);
return renderPage;
}
This is obviously just an example of what my code does, and is not the exact file since the original is thousands of lines long.

Don't do it on render(). Instead, you can do it using the hook componentDidMount() inside your class, like this:
componentDidMount() {
if(this.state.StoreInfo.length === 0) {
//set your state here
}
}
Inside your if statement, you can verify the length of some data to prevent it to enter in the render loop, don't need to be the length of some information array inside your state, but something that flags to your code that you already loaded up the information. You can also create a bool for this and set it to true (or false) when you load your data.

Related

Conditional Rendering of Arrays in React: For vs Map

I'm new to React and building a calendar application. While playing around with state to try understand it better, I noticed that my 'remove booking' function required a state update for it to work, while my 'add booking' function worked perfectly without state.
Remove bookings: requires state to work
const [timeslots, setTimeslots] = useState(slots);
const removeBookings = (bookingid) => {
let newSlots = [...timeslots];
delete newSlots[bookingid].bookedWith;
setTimeslots(newSlots);
}
Add bookings: does not require state to work
const addBookings = (slotid, tutorName) => {
timeslots[slotid].bookedWith = tutorName;
}
I think that this is because of how my timeslot components are rendered. Each slot is rendered from an item of an array through .map(), as most tutorials online suggest is the best way to render components from an array.
timeslots.map(slot => {
if (!slot.bookedWith) {
return <EmptyTimeslot [...props / logic] />
} else {
return <BookedTimeslot [...props / logic]/>
}
})
So, with each EmptyTimeslot, the data for a BookedTimeslot is available as well. That's why state is not required for my add bookings function (emptyTimeslot -> bookedTimeslot). However, removing a booking (bookedTimeslot -> emptyTimeslot) requires a rerender of the slots, since the code cannot 'flow upwards'.
There are a lot of slots that have to be rendered each time. My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots? This I assume would require state to be used for both the add booking and remove booking function. Like this:
for (let i=0;i<timeslots.length;i++) {
if (!timeslot[i].bookedWith) {
return <EmptyTimeslot />
} else {
return <BookedTimeslot />
}
}
Hope that makes sense. Thank you for any help.
Your addBooking function is bad. Even if it seems to "work", you should not be mutating your state values. You should be using a state setter function to update them, which is what you are doing in removeBookings.
My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots?
Your map approach is not rendering both. For each slot, it uses an if statement to return one component or the other depending on whether the slot is booked. I'm not sure how the for loop you're proposing would even work here. It would just return before the first iteration completed.
This I assume would require state to be used for both the add booking and remove booking function.
You should be using setTimeslots for all timeslot state updates and you should not be mutating your state values. That is true no matter how you render them.

Getting React errors when traversing an in-scope JSON object via Dot Notation [duplicate]

This question already has answers here:
How to handle calling functions on data that may be undefined?
(2 answers)
Closed 3 years ago.
render() {
return (
<p>{this.state.recipes[0].title}</p> // This is what I believe should work - Ref Pic #1
// <p>{this.state.recipes[0]}</p> // This was trying random stuff to see what happens - Ref Pic #2
// <p>{this.state.recipes.title}</p> // This was me trying crazy nonsense - Ref Pic #3
)
}
I am attempting to traverse through some JSON and am getting some very wonky responses. If anyone would like to look at the JSON themselves, it's available at this link.
When the first p tag is run, I get the following response:
This is my first question, so I can't embed images, I'm sorry.
Being unsure why it said recipes[0] was undefined, I ran it again with the second p tag soley uncommented, to which I get the following response: Still the same question, still can't embed, sorry again.
That response really caught me off guard because the keys it reference's (nid, title, etc..) are the ones I know are in the object. 'Title' is what I want.
Last, I tried just the third p tag, to which the app actually compiled with no errors, but the p tag was empty. Here's what I have in React Devtools, but I still can't embed.
Looking at < Recipes > it clearly shows that state has exactly what I want it to, and image two shows recipes[0] has the key I try to call the first time. I have searched and Googled and even treated my own loved ones as mere rubberducks, but to no avail.
What am I doing wrong?
The error in the first p tag tells you what exactly is happening. Accessing a property of an 'undefined' will break javascript/ react.
You must probably not have the expected data in the state, to verify what I am saying, simply debug by console.log(this.state) in your render:
render() {
console.log(this.state); // probably has this.state.recipes empty array at first render
...
this probably happened because your state is being populated by an async call from an api. Render will be called first before your async request from an api resolves (which means unfortunately you don't have the ..recipes[0] data yet in your state)
I am guessing you have this in your componentDidMount:
componentDidMount() {
ThirdParty.fetchRecipes().then(data => {
this.setState({ recipes: data.recipes }); // after setState react calls render again. This time your p tag should work.
})
...
If you have newer react version I would recommend to use optional chaining because the property you are interested in is deeply nested: How to handle calling functions on data that may be undefined?
<p>{this.state?.recipes?.[0]?.title}</p>
Another effective way but too wordy:
const title =
this.state &&
this.state.recipes &&
this.state.recipes.length &&
this.state.recipes[0].title
? this.state.recipes[0].title
: "";
render() {return (<p>{title}</p>)}
I believe you are fetching the JSON asynchronously. In that case you should consider boundary cases where your initial recipes state is an empty array.
When you run the first p tag, since JSON is fetched asynchronously, initially this.state.recipes is not defined, hence you get the error.
When you run the second p tag, during the first render, this.state.recipes is undefined, hence it works inside p tag, but after the JSON is fetched and state is set, render is called again and this time this.state.recipes[0] is an object, hence it can't be counted as a valid react component, thus the error shows up.
With the 3rd p tag, as you have mentioned yourself, this.state.recipes.title will compile successfully with no errors, but the value will be undefined.
You just need to check for the edge cases where initial state is empty.
constructor(props) {
super(props);
this.state = { recipes: [] }
}
render() {
return (
<p>
{this.state.recipes.length > 0 ? {this.state.recipes[0].title} : ''}
</p>
)
}
you can do something like this to handle state when there is data and when there is no data
constructor(props) {
super(props);
this.state = {
recipes: []
};
}
render() {
return (
<p>
{this.state.recipes && this.state.recipes.length ? {this.state.recipes[0].title} : {this.state.recipes}}
</p>
)
}

NextJS - can't identify where state change is occurring on page component when using shallow Router push

I have an app. that uses NextJS. I have a page that looks like the following:
import React from 'react'
import { parseQuery } from '../lib/searchQuery'
import Search from '../components/search'
class SearchPage extends React.Component {
static getInitialProps ({ query, ...rest }) {
console.log('GET INITIAL PROPS')
const parsedQuery = parseQuery(query)
return { parsedQuery }
}
constructor (props) {
console.log('CONSTRUCTOR OF PAGE CALLED')
super(props)
this.state = props.parsedQuery
}
render () {
return (
<div>
<div>
<h1>Search Results</h1>
</div>
<div>
<h1>DEBUG</h1>
<h2>PROPS</h2>
{JSON.stringify(this.props)}
<h2>STATE</h2>
{JSON.stringify(this.state)}
</div>
<div>
<Search query={this.state} />
</div>
</div>
)
}
}
export default SearchPage
getInitialProps is ran for SSR - it receives the query string as an object (via Express on the back end) runs it through a simple 'cleaner' function - parseQuery - which I made, and injects it into the page via props as props.parsedQuery as you can see above. This all works as expected.
The Search component is a form with numerous fields, most of which are select based with pre-defined fields and a few a number based input fields, for the sake of brevity I've omitted the mark up for the whole component. Search takes the query props and assigns them to its internal state via the constructor function.
On changing both select and input fields on the Search component this code is ran:
this.setState(
{
[label]: labelValue
},
() => {
if (!this.props.homePage) {
const redirectObj = {
pathname: `/search`,
query: queryStringWithoutEmpty({
...this.state,
page: 1
})
}
// Router.push(href, as, { shallow: true }) // from docs.
this.props.router.push(redirectObj, redirectObj, { shallow: true })
}
}
)
The intention here is that CSR takes over - hence the shallow router.push. The page URL changes but getInitialProps shouldn't fire again, and subsequent query changes are handled via componentWillUpdate etc.. I confirmed getInitialProps doesn't fire again by lack of respective console.log firing.
Problem
However, on checking/unchecking the select fields on the Search component I was surprised to find the state of SearchPage was still updating, despite no evidence of this.setState() being called.
constructor isn't being called, nor is getInitialProps, so I'm unaware what is causing state to change.
After initial SSR the debug block looks like this:
// PROPS
{
"parsedQuery": {
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
Then after checking a select field in Search surprisingly it updates to this:
// PROPS
{
"parsedQuery": {
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
I can't find an explanation to this behaviour, nothing is output to the console and I can't find out how to track state changes origins via dev. tools.
Surely the state should only update if I were to do so via componentDidUpdate? And really shouldn't the parsedQuery prop only ever be updated by getInitialProps? As that's what created and injected it?
To add further confusion, if I change a number input field on Search (such as lowPrice), the URL updates as expected, but props nor page state changes in the debug block. Can't understand this inconsistent behaviour.
What's going on here?
EDIT
I've added a repo. which reproduces this problem on as a MWE on GitHub, you can clone it here: problem MWE repo.
Wow, interesting problem. This was a fun little puzzle to tackle.
TL;DR: This was your fault, but how you did it is really subtle. First things first, the problem is on this line:
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L17
Here in this example, it is this:
this.state = props.parsedQuery
Let's consider what is actually happening there.
In IndexPage.getInitialProps you are doing the following:`
const initialQuery = parseQuery({ ...query })
return { initialQuery }
Through Next's mechanisms, this data passes through App.getInitialProps to be returned as pageProps.initialQuery, which then becomes props.initialQuery in IndexPage, and which is then being passed wholesale through to your Search component - where your Search component then "makes a copy" of the object to avoid mutating it. All good, right?
You missed something.
In lib/searchQuery.js is this line:
searchQuery[field] = []
That same array is being passed down into Search - except you aren't copying it. You are copying props.query - which contains a reference to that array.
Then, in your Search component you do this when you change the checkbox:
const labelValue = this.state[label]
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L57
You're mutating the array you "copied" in the constructor. You are mutating your state directly! THIS is why initialQuery appears to update on the home page - you mutated the manufacturers array referenced by initialQuery - it was never copied. You have the original reference that was created in getInitialProps!
One thing you should know is that even though getInitialProps is not called on shallow pushes, the App component still re-renders. It must in order to reflect the route change to consuming components. When you are mutating that array in memory, your re-render reflects the change. You are NOT mutating the initialQuery object when you add the price.
The solution to all this is simple. In your Search component constructor, you need a deep copy of the query:
this.state = { ...cloneDeep(props.query) }
Making that change, and the issue disappears and you no longer see initialQuery changing in the printout - as you would expect.
You will ALSO want to change this, which is directly accessing the array in your state:
const labelValue = this.state[label]
to this:
const labelValue = [...this.state[label]]
In order to copy the array before you change it. You obscure that problem by immediately calling setState, but you are in fact mutating your component state directly which will lead to all kinds of weird bugs (like this one).
This one arose because you had a global array being mutated inside your component state, so all those mutations were being reflected in various places.

Reactjs: how to write a method to handle component creation and unmount

So let's say there is acomponent which displays 2 child components: a document list and the selected document. By default the selected document component is not rendered, only when a document is selected from the list. And i also want this whole thing work when a new document is selected from the list.
There is a state which holds the document content and responsible for the selected document rendering, so i thought i'm going to set it to null in the method which handles the list item selection in order to unmount the previously created child component. Like this (excerpts from the parent class):
handleResultListItemClick(docname) {
if (this.state.sectioncontainer != null) this.setState({sectioncontainer: null},()=>{console.log("muhoo");});
var selected_doc = this.state.resultlist.filter((doc) => {
return docname === doc.properties.title;
});
this.setState({sectioncontainer: selected_doc[0].content.sections},()=>{console.log("boohoo");});
}
...
render() {
return (
...
{this.state.sectioncontainer != null && <SectionContainer listOfSections={this.state.sectioncontainer}/>}
);
}
The only problem is that state handling is not fast enough (or something) in react, because putting the state nullification and its new value setting in the same method results in no change in ReactDOM.
With the above code, the component will be created when the parent component first rendered, but after selecting a new doc in the list results in no change.
How should i implement this in way which works and also elegant?
I found this: ReactDOM.unmountComponentAtNode(container) in the official react docs. Is this the only way? If yes, how could i get this container 'name'?
Edit:
Based on the answers and thinking the problem a bit more through, i have to explain more of the context.
As kingdaro explained, i understand why there is no need to unmount a child component on a basic level, but maybe my problem is bit more sophisticated. So why did i want to unmount the child?
The documents consist of several subsections, hence the document object which is passed to the child component is an array of objects. And the document is generated dynamically based on this array the following way (excerpt from the SectionContainer class which is responsible to display the document):
buildSectionContainer() {
return this.props.listOfSections.map((section, index) =>
{
if (section.type === 'editor') return (
<QuillEditor
key={index}
id={section.id}
modules={modules}
defaultValue={section.content}
placeholder={section.placeholder}
/>
);
else if (section.type === 'text') return (
<div key={index}>{section.value}</div>
);
}
);
}
render() {
return (
<div>
{this.buildSectionContainer()}
</div>
);
}
The SectionContainer gets the array of objects and generate the document from it according to the type of these sections. The problem is that these sections are not updated when a different doc is selected in the parent component. I see change only when a bigger length array is passed to the child component. Like the firstly selected doc had an array of 2 elements, and then the newly selected doc had 3 elements array of sections and this third section is added to the previously existing 2, but the first 2 sections remained as they were.
And that’s why i though it’s better to unmount the child component and create a new one.
Surely it can happen that i miss something fundamental here again. Maybe related to how react handles lists. I just dont know what.
Edit2:
Ok, figured out that there is a problem with how i use the QuillEditor component. I just dont know what. :) The document updates, only the content of QuillEditors doesnt.
The reason your current solution doesn't actually do anything is because React's state updates are batched, such that, when setState is called a bunch of times in one go, React "combines" the result of all of them. It's not as much of a problem with being "not fast enough" as it is React performing only the work that is necessary.
// this...
this.setState({ message: 'hello', secret: 123 })
this.setState({ message: 'world' })
// ...becomes this
this.setState({ message: 'world', secret: 123 })
This behavior doesn't really have much to do with the problem at hand, though. As long as your UI is a direct translation of state -> view, the UI should simply update in accordance to the state.
class Example extends React.Component {
state = {
documentList: [], // assuming this comes from the server
document: null,
}
// consider making this function accept a document object instead,
// then you could leave out the .find(...) call
handleDocumentSelection = documentName => {
const document = this.state.documentList.find(doc => doc.name === documentName)
this.setState({ document })
}
render() {
const { document } = this.state
return (
<div>
<DocumentList
documents={this.state.documentList}
onDocumentSelection={this.handleDocumentSelection}
/>
{/*
consider having this component accept the entire document
to make it a little cleaner
*/}
{document && <DocumentViewer document={document.content.sections} />}
</div>
)
}
}

Fill a container with content using React.JS

I have a container element, which is also a React component, with a specific height. I also have an API that returns blocks of contents of variable length, based on the requested ID.
<Contents/>
I want to request a new block of content from the API until the container is overflowing.
My code works, but it rerenders all content blocks and adds one in each render, until the container is full. This seems like a bad approach.
class Contents extends React.Component {
constructor() {
super();
this.state = {
numElements: 0
};
}
render() {
const elements = [];
for(let i = 0; i < this.state.numElements; i++) {
elements.push(this._getElementContents(i));
}
return(<div id="contents">
{elements.map(element => element)}
</div>);
}
componentDidMount() {
// start the 'filling loop'
this._addElement();
}
componentDidUpdate() {
// keep adding stuff until container is full
if(document.getElementById('contents').clientHeight < window.outerHeight - 400) {
this._addElement();
}
}
_addElement() {
// setState will cause render() to be called again
this.setState({numElements: this.state.numElements + 1});
}
_getElementContents(i) {
// simplified, gets stuff from API:
let contents = api_response;
return(<Element key={i} body={contents} />);
}
}
How can I append elements to the container until it is filled, without re-adding, re-querying the API and re-rendering existing elements on each loop?
I can't see how you are calling your API or under what condition. From my understanding you should do two things.
Keep elements array into the state object and push new elements whenever they arrive.
Use the shouldComponentUpdate instead of componentDidUpdate with the same exactly condition to judge when you have to request more elements from your API.
Eventually draw the state.elements. Whenever you receive a new one you just use the local elements you previously got to redraw the component instead of making all API calls all over again
Because this.setState() triggers a re-render, like dimitrisk said, you should put logic in shouldComponentUpdate to only render once you have everything ready to go to the DOM.
You should look into Dynamic Children in React. It helps React do reconciliation when re-rendering http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
http://revelry.co/dynamic-child-components-with-react/

Categories

Resources