React state change does not result in a rerender - javascript

I'm using React Infinite Scroller to render a list of posts.
My load more results function is called 3 times and therefore the result is updated three times (can be seen with a console.log)
I render the posts with:
{results.map((result) => (
<Widget data={result} key={result.id} />
))}
This updates the result the first two times, but when the state changes for the third time, it doesn't render any more results.
My full code:
import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
import LoadingIndicator from "./loading";
import { request } from "../communication";
const InfiniteResults = ({ endpoint, widget }) => {
let [results, setResults] = useState([]);
let [hasMoreResults, setHasMoreResults] = useState(true);
const Widget = widget;
function loadMoreResults(page) {
// prevent from making more requests while waiting for the other one to complete
setHasMoreResults(false);
request
.get(endpoint, {
offset: page * 10,
})
.then(function gotResults(response) {
const data = response.data;
const resultsCopy = results;
data.results.forEach(function saveResultToState(result) {
resultsCopy.push(result);
});
setResults(resultsCopy);
console.log(resultsCopy);
console.log(results);
if (!data.next) {
setHasMoreResults(false);
} else {
setHasMoreResults(true);
}
});
}
return (
<InfiniteScroll
pageStart={-1}
loadMore={loadMoreResults}
hasMore={hasMoreResults}
loader={<p key={0}>Loading...</p>}
>
{results.map((result) => {
console.log("rerender");
return <Widget data={result} key={result.id} />;
})}
</InfiniteScroll>
);
};
export default InfiniteResults;

Your problem is with your state update:
const resultsCopy = results;
data.results.forEach(function saveResultToState(result) {
resultsCopy.push(result);
);
setResults(resultsCopy);
When you do const resultsCopy = results;, you're not actually copying the array; you're just referring to the same array by another name. That means that when you start adding elements to it, you are adding them directly to the one controlled by React's useState hook. All state updates should be done through the setState (setResults in your case) function that useState returns.
To perform an update on the previous state, the best way to do that is to call the setResults function with a function as an argument, that takes the previous state as its own argument. This ensures that the state updates are sequential (each update happens after the previous one was completed).
setResults(prevState => prevState.concat(data.results));
// or
setResults(function (prevState) {
return prevState.concat(data.results);
});
Here I have also made use of the concat function that combines two arrays into one and returns the result. That way you are not updating the previous state with the push calls.

Related

How to trigger re-render in Parent Component from Child Component

, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)

In React how can I append values to functional state when it is a dependency of useEffect without triggering another API call?

I have a todo list app which users can read and save items to. Here, Todo is a functional component that queries an API for the users current items on their list using the useEffect() hook. When a successful response is received the data is added to the component's state using useState() and rendered as part of the ItemList component.
When a user submits the form within the AddItemForm component a call back is fired that updates the state of newItem, a dependency of useEffect, which triggers another call to the API and a re-render of the component.
Logically, everything above works. However, it seems wrong to make an extra request to the API simply to receive the data that is already available but I can't find the correct pattern that would allow me to push the item available in useCallback to the items array without causing useEffect to loop infinitely yet still update the ItemList component.
Is there away for my app to push new date from the form submission to items array whilst updating the view and only calling the API once when the page loads?
function Todo() {
const [items, setItems] = useState();
const [newItem, setNewItem] = useState();
useEffect(() => {
fetch('https://example.com/items').then(
(response) => {
setItems(response.data.items);
}, (error) => {
console.log(error);
},
);
}, [newItem]);
const updateItemList = useCallback((item) => {
setNewItem(item);
});
return (
<>
<AddItemForm callback={updateItemList} />
<ItemList items={items} />
</>
);
}
function ItemList(props) {
const { items } = props;
return (
<div>
{ items
&& items.map((item) => <p>{item.description}</p>)}
</div>
);
}
Call API only on start by removing newItem from useEffect(...,[]).
Then add item to the items by destructuring in setItems:
const updateItemList = (item) => {
setItems([...items, item]);
}

Seeking advice: Reason for maximum update depth exceed

I am new to React and GraphQL. Trying to update React state with GraphQL subscription feed but it generates the update depth error.
Here is the simplified code:
import { Subscription } from 'react-apollo';
...
function Comp() {
const [test, setTest] = useState([]);
const Sub = function() {
return (
<Subscription subscription={someStatement}>
{
result => setTest(...test, result.data);
return null;
}
</Subscription>
);
};
const Draw = function() {
return (
<div> { test.map(x => <p>{x}</p>) } </div>
);
};
return (
<div>
<Sub />
<Draw />
<div/>
);
};
export default Comp;
Regular query works fine in the app and the Subscription tag returns usable results, so I believe the problem is on the React side.
I assume the displayed code contains the source of error because commenting out the function "Sub" stops the depth error.
You see what happens is when this part renders
<Subscription subscription={someStatement}>
{
result => setTest(...test, result.data);
return null;
}
</Subscription>
setTest() is called and state is set which causes a re-render, that re-render cause the above block to re-render and setTest() is called again and the loop goes on.
Try to fetch and setTest() in your useEffect() Hook so it does not gets stuck in that re-render loop.
useEffect like
useEffect(() => {
//idk where result obj are you getting from but it is supposed to be
//like this
setTest(...test, result.data);
}, [test] )
Component Like
<Subscription subscription={someStatement} />

React: how can I force state to update in a functional component?

This function component has a template method that calls onChangeHandler, which accepts a select value and updates state. The problem is, state does not update until after the render method is called a second time, which means the value of selected option is one step ahead of the state value of selectedRouteName.
I know there are lifecycle methods in class components that I could use to force a state update, but I would like to keep this a function component, if possible.
As noted in the code, the logged state of selectedRouteDirection is one value behind the selected option. How can I force the state to update to the correct value in a functional component?
This question is not the same as similarly named question because my question asks about the actual implementation in my use case, not whether it is possible.
import React, { useState, Fragment, useEffect } from 'react';
const parser = require('xml-js');
const RouteSelect = props => {
const { routes } = props;
const [selectedRouteName, setRouteName] = useState('');
const [selectedRouteDirection, setRouteDirection] = useState('');
//console.log(routes);
const onChangeHandler = event => {
setRouteName({ name: event.target.value });
if(selectedRouteName.name) {
getRouteDirection();
}
}
/*
useEffect(() => {
if(selectedRouteName) {
getRouteDirection();
}
}); */
const getRouteDirection = () => {
const filteredRoute = routes.filter(route => route.Description._text === selectedRouteName.name);
const num = filteredRoute[0].Route._text;
let directions = [];
fetch(`https://svc.metrotransit.org/NexTrip/Directions/${num}`)
.then(response => {
return response.text();
}).then(response => {
return JSON.parse(parser.xml2json(response, {compact: true, spaces: 4}));
}).then(response => {
directions = response.ArrayOfTextValuePair.TextValuePair;
// console.log(directions);
setRouteDirection(directions);
})
.catch(error => {
console.log(error);
});
console.log(selectedRouteDirection); // This logged state is one value behind the selected option
}
const routeOptions = routes.map(route => <option key={route.Route._text}>{route.Description._text}</option>);
return (
<Fragment>
<select onChange={onChangeHandler}>
{routeOptions}
</select>
</Fragment>
);
};
export default RouteSelect;
Well, actually.. even though I still think effects are the right way to go.. your console.log is in the wrong place.. fetch is asynchronous and your console.log is right after the fetch instruction.
As #Bernardo states.. setState is also asynchronous
so at the time when your calling getRouteDirection();, selectedRouteName might still have the previous state.
So to make getRouteDirection(); trigger after the state was set.
You can use the effect and pass selectedRouteName as second parameter (Which is actually an optimization, so the effect only triggers if selectedRouteName has changed)
So this should do the trick:
useEffect(() => {
getRouteDirection();
}, [selectedRouteName]);
But tbh.. if you can provide a Stackblitz or similar, where you can reproduce the problem. We can definitely help you better.
setState is asynchronous! Many times React will look like it changes the state of your component in a synchronous way, but is not that way.

How to convert a React class component to a function component with hooks to get firebase data

I have a working React class component that I want to convert to a functional component to use hooks for state etc. I am learning React hooks. The class component version works fine, the functional component is where I need help.
The data structure consists of a client list with three "clients". An image of it is here:
All I am trying to do is get this data, iterate over it and display the data of each name key to the user. Simple enough.
The problem is that a call to firebase from my component leads to erratic behavior in that the data is not retrieved correctly. The last client name is continuously called and it freezes up the browser. :)
Here is an image of the result:
Here is the code:
import React, {Component,useContext,useEffect, useState} from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Grid from '#material-ui/core/Grid';
import ListItem from '#material-ui/core/ListItem';
import Button from '#material-ui/core/Button';
import firebase from 'firebase/app';
import {Consumer,Context} from '../../PageComponents/Context';
const styles = theme => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
});
const FetchData = (props) =>{
const [state, setState] = useState(["hi there"]);
const userID = useContext(Context).userID;
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
});
//____________________________________________________BEGIN NOTE: I am emulating this code from my class component and trying to integrate it
// this.clientsRef.on('child_added', snapshot => {
// const client = snapshot.val();
// client.key = snapshot.key;
// this.setState({ clients: [...this.state.clients, client]})
// });
//___________________________________________________END NOTE
console.log(state)
return (
<ul>
{
state.map((val,index)=>{
return <a key={index} > <li>{val.name}</li> </a>
})
}
</ul>
)
}
FetchData.propTypes = {
classes: PropTypes.object.isRequired
}
export default withStyles(styles)(FetchData)
By default, useEffect callback is run after every completed render (see docs) and you're setting up a new firebase listener each such invocation. So when the Firebase emits the event each of such listeners receives the data snapshot and each of them adds to the state a received value.
Instead you need to set the listener once after component is mounted, you can do so by providing an empty array of the dependencies ([]) as a second argument to useEffect:
useEffect(() => {
// your code here
}, []) // an empty array as a second argument
This will tell React that this effect doesn't have any dependencies so there is no need to run it more than once.
But there is another one important moment. Since you setup a listener then you need to clean it up when you don't need it anymore. This is done by another callback that you should return in the function that you pass to useEffect:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
return () => clientsRef.off('child_added') // unsubscribe on component unmount
}, []);
Basically this returned cleanup function will be invoked before every new effect is called and right before a component unmounts (see docs) so only this cleanup function should solve your solution by itself, but there's no need to call your effect after every render anyway hence [] as a second argument.
Your problem is that by default, useEffect() will run every single time your component renders. What is happening, is that your effect triggers a change in the component, which will trigger the effect running again and you end up with something approximating an endless loop.
Luckily react gives us some control over when to run the effect hook in the form of an array you can pass in as an additional parameter. In your case for example:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
}, []);//An empty array here means this will run only once.
The array tells react which properties to watch. Whenever one of those properties changes it will run the cleanup function and re-run the effect. If you submit an empty array, then it will only run once (since there are no properties to watch). For example, if you were to add [userId] the effect would run every time the userId variable changes.
Speaking of cleanup function, you are not returning one in your effect hook. I'm not familiar enough with firebase to know if you need to clean anything up when the component is destroyed (like for example remove the 'child_added' event binding). It would be good practice to return a method as the last part of your use effect. The final code would look something like:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
return () => { /* CLEANUP CODE HERE */ };
}, []);//An empty array here means this will run only once.
Effects, by default, run after every render, and setting state causes a render. Any effect that updates state needs to have a dependency array specified, otherwise you'll just have an infinite update-render-update-render loop.
Also, remember to clean up any subscriptions that effects create. Here, you can do that by returning a function which calls .off(...) and removes the listener.
Then, make sure to use the function form of state update, to make sure the next state always relies on the current state, instead of whatever the closure value happened to be when binding the event. Consider using useReducer if your component's state becomes more complex.
const [clients, setClients] = useState([])
useEffect(() => {
const clientsRef = firebase.database().ref("clients")
const handleChildAdded = (snapshot) => {
const client = snapshot.val()
client.key = snapshot.key
setClients(clients => [...clients, client])
}
clientsRef.on("child_added", handleChildAdded)
return () => clientsRef.off('child_added', handleChildAdded)
}, [])
Also see:
How to fetch data with hooks
React Firebase Hooks
A complete guide to useEffect

Categories