React component not re-rendering on state change - javascript

I have a React Class that's going to an API to get content. I've confirmed the data is coming back, but it's not re-rendering:
var DealsList = React.createClass({
getInitialState: function() {
return { deals: [] };
},
componentDidMount: function() {
this.loadDealsFromServer();
},
loadDealsFromServer: function() {
var newDeals = [];
chrome.runtime.sendMessage({ action: "findDeals", personId: this.props.person.id }, function(deals) {
newDeals = deals;
});
this.setState({ deals: newDeals });
},
render: function() {
var dealNodes = this.state.deals.map(function(deal, index) {
return (
<Deal deal={deal} key={index} />
);
});
return (
<div className="deals">
<table>
<thead>
<tr>
<td>Name</td>
<td>Amount</td>
<td>Stage</td>
<td>Probability</td>
<td>Status</td>
<td>Exp. Close</td>
</tr>
</thead>
<tbody>
{dealNodes}
</tbody>
</table>
</div>
);
}
});
However, if I add a debugger like below, newDeals are populated, and then once I continue, i see the data:
loadDealsFromServer: function() {
var newDeals = [];
chrome.runtime.sendMessage({ action: "findDeals", personId: this.props.person.id }, function(deals) {
newDeals = deals;
});
debugger
this.setState({ deals: newDeals });
},
This is what's calling deals list:
var Gmail = React.createClass({
render: function() {
return (
<div className="main">
<div className="panel">
<DealsList person={this.props.person} />
</div>
</div>
);
}
});

I'd like to add to this the enormously simple, but oh so easily made mistake of writing:
this.state.something = 'changed';
... and then not understanding why it's not rendering and Googling and coming on this page, only to realize that you should have written:
this.setState({something: 'changed'});
React only triggers a re-render if you use setState to update the state.

My scenario was a little different. And I think that many newbies like me would be stumped - so sharing here.
My state variable is an array of JSON objects being managed with useState as below:
const [toCompare, setToCompare] = useState([]);
However when update the toCompare with setToCompare as in the below function - the re-render won't fire. And moving it to a different component didn't work either. Only when some other event would fire re-render - did the updated list show up.
const addUniversityToCompare = async(chiptoadd) =>
{
var currentToCompare = toCompare;
currentToCompare.push(chiptoadd);
setToCompare(currentToCompare);
}
This was the solution for me. Basically - assigning the array was copying the reference - and react wouldn't see that as a change - since the ref to the array isn't being changed - only content within it. So in the below code - just copied the array using slice - without any change - and assigned it back after mods. Works perfectly fine.
const addUniversityToCompare = async (chiptoadd) => {
var currentToCompare = toCompare.slice();
currentToCompare.push(chiptoadd);
setToCompare(currentToCompare);
}
Hope it helps someone like me. Anybody, please let me know if you feel I am wrong - or there is some other approach.
Thanks in advance.

That's because the response from chrome.runtime.sendMessage is asynchronous; here's the order of operations:
var newDeals = [];
// (1) first chrome.runtime.sendMessage is called, and *registers a callback*
// so that when the data comes back *in the future*
// the function will be called
chrome.runtime.sendMessage({...}, function(deals) {
// (3) sometime in the future, this function runs,
// but it's too late
newDeals = deals;
});
// (2) this is called immediately, `newDeals` is an empty array
this.setState({ deals: newDeals });
When you pause the script with the debugger, you're giving the extension time to call the callback; by the time you continue, the data has arrived and it appears to work.
To fix, you want to do the setState call after the data comes back from the Chrome extension:
var newDeals = [];
// (1) first chrome.runtime.sendMessage is called, and *registers a callback*
// so that when the data comes back *in the future*
// the function will be called
chrome.runtime.sendMessage({...}, function(deals) {
// (2) sometime in the future, this function runs
newDeals = deals;
// (3) now you can call `setState` with the data
this.setState({ deals: newDeals });
}.bind(this)); // Don't forget to bind(this) (or use an arrow function)
[Edit]
If this doesn't work for you, check out the other answers on this question, which explain other reasons your component might not be updating.

Another oh-so-easy mistake, which was the source of the problem for me: I’d written my own shouldComponentUpdate method, which didn’t check the new state change I’d added.

To update properly the state, you shouldn't mutate the array. You need to create a copy of the array and then set the state with the copied array.
const [deals, setDeals] = useState([]);
function updateDeals(deal) {
const newDeals = [...deals]; // spreading operator which doesn't mutate the array and returns new array
newDeals.push(deal);
// const newDeals = deals.concat(deal); // concat merges the passed value to the array and return a new array
// const newDeals = [...deals, deal] // directly passing the new value and we don't need to use push
setDeals(newDeals);
}

In my case, I was calling this.setState({}) correctly, but I my function wasn't bound to this, so it wasn't working. Adding .bind(this) to the function call or doing this.foo = this.foo.bind(this) in the constructor fixed it.

My issue was that I was using 'React.PureComponent' when I should have been using 'React.Component'.

I was updating and returning the same object passed to my reducer. I fixed this by making a copy of the element just before returning the state object like this.
Object.assign({}, state)

I was going through same issue in React-Native where API response & reject weren't updating states
apiCall().then(function(resp) {
this.setState({data: resp}) // wasn't updating
}
I solved the problem by changing function with the arrow function
apiCall().then((resp) => {
this.setState({data: resp}) // rendering the view as expected
}
For me, it was a binding issue. Using arrow functions solved it because arrow function doesn't create its's own this, its always bounded to its outer context where it comes from

After looking into many answers (most of them are correct for their scenarios) and none of them fix my problem I realized that my case is a bit different:
In my weird scenario my component was being rendered inside the state and therefore couldn't be updated.
Below is a simple example:
constructor() {
this.myMethod = this.myMethod.bind(this);
this.changeTitle = this.changeTitle.bind(this);
this.myMethod();
}
changeTitle() {
this.setState({title: 'I will never get updated!!'});
}
myMethod() {
this.setState({body: <div>{this.state.title}</div>});
}
render() {
return <>
{this.state.body}
<Button onclick={() => this.changeTitle()}>Change Title!</Button>
</>
}
After refactoring the code to not render the body from state it worked fine :)

If someone is here for similar problem, but using React Functional components rather class components AND also using react reducer, --> Move your api call outside of the reducer. Reducer should never do an api call. Refer to https://stackoverflow.com/a/39516485/12121297 for detailed response

In my case the issue was related to a child component.
In a nutshell:
Parent component updates the state of an array of objects and
forwards the updated array to a child component.
The child component receives the array, creates a copy of if (spread
operator) and uses it to update an internal stateful component which
is supposed to be rendered.
However, react does not re-render the child component when the update
occurs in the parent component.
I solved my problem by adding a useEffect hook in the child components.
I still don’t really understand how the state update is handled in my case.
The code is available in my stackblitz repo here:
https://stackblitz.com/edit/react-hel9yv?file=src%2FApp.js

Related

Javascript object vs array ask Axios.get [duplicate]

Is there a reason that calling setSate() in a loop would prevent it from updating the state multiple times?
I have a very basic jsbin that highlights the problem I am seeing. There are two buttons. One updates the state's counter by 1. The other calls the underlying function of One in a loop -- which seemingly would update the state multiple times.
I know of several solutions to this problem but I want to make sure that I am understanding the underlying mechanism here first. Why can't setState be called in a loop? Do I have it coded awkwardly that is preventing the desired effect?
From the React Docs:
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Basically, don't call setState in a loop. What's happening here is exactly what the docs are referring to: this.state is returning the previous value, as the pending state update has not been applied yet.
There's a nice way to update state in a loop. Just make an empty variable, set its value to the updated state, call setState(), and pass it this variable:
const updatedState = {};
if (vars.length) {
vars.forEach(v => {
updatedState[v] = '';
this.setState({
...this.state
...updatedState,
});
});
}
You have to use something like that:
const MyComponent = () => {
const [myState, setMyState] = useState([]);
const handleSomething = (values) => {
values.map((value) => {
setMyState((oldValue) => [...oldValue, { key: value.dataWhatYouWant }]);
}
}
return (<> Content... </>);
}
I had the same problem. But tried with a little different approach.
iterateData(data){
//data to render
let copy=[];
for(let i=0;<data.length;i++){
copy.push(<SomeComp data=[i] />)
}
this.setState({
setComp:copy
});
}
render(){
return(
<div>
{this.state.setComp}
</div>
);
}
I hope this helps.
Basically setState is called asynchronously. It also has a callback function which you can utilise to do something once the state has been mutated.
Also if multiple setStates are called one after the other they are batched together as written previously.
Actually setState() method is asynchronous. Instead you can achieve it like this
manyClicks() {
var i = 0;
for (i = 0; i < 100; i++) {
//this.setState({clicks: this.state.clicks + 1}); instead of this
this.setState((prevState,props)=>({
clicks: ++prevState.clicks
}))
}
}
I was having this issue when creating a feature to import items.
Since the amount of the importing items could be huge, I need to provide feedback (like a progress bar) to the site user so that they know that they aren't sitting there and waiting for nothing.
As we know that we can't setState in a loop, I took a different approach by running the task recursively.
Here's a example code
https://codesandbox.io/s/react-playground-forked-5rssb
You can try this one using the previous value to increase the count.
function handleChange() {
for (let i = 0; i < 5; i++) {
setState(prev => {
return prev + 1
})
}
}
I was able to make your code work, calling setState in the loop by doing the following:
manyClicks() {
for (i = 0; i < 100; i++) {
this.setState({clicks: this.state.clicks += 1})
}
}
enter code here
Hopefully this helps!

Do react query result objects go stale due to closures?

I have always felt like I have a pretty solid understanding of how JS closures work, but since using ReactJS for the last couple of months there is something I can't quite make sense of.
In the below example, will the query result object getData be stale if used in the success callback for the other query?
My closure instincts tell me yes, but my reactjs instincts tell me no for some reason and I can't make sense of why it works the way that it does.
All help is appreciated!
/*
* Custom query hooks
*/
function useGetData(someId, opts){
return useQuery(['getData', someId],
getDataQueryFn,
{
...opts,
},
);
}
function useOtherData(someId, opts){
return useQuery(['otherData', someId],
otherDataQueryFn,
{
...opts,
},
);
}
/*
* React component
*/
function MyComponent(){
const getData = useGetData("1");
const otherData = useOtherData("1", {
onSuccess: () => {
// QUESTION: if I use getData here, won't it be stale due to closure? why or why not?
}
});
// render and stuff...
}
It doesn't go stale. Here's a codesandbox that closures over a counter and the fetch needs 5 seconds to complete. relevant code from the sandbox:
const [count, inc] = React.useReducer((val) => val + 1, 0);
const { data } = useQuery(
"repoData",
async () => {
await waitFor(5000);
return Promise.resolve({ name: "foo " });
},
{
onSuccess: () => console.log(count)
}
);
if you click the counter a couple of times in the first 5 seconds, the latest count will still be logged. It doesn't really matter if you closure over local state or over some other query.
The reason is likely that react-query will only trigger a re-render after the Promise is successful (it's a simple forced re-render via useState under the hood), and at that time, react already "sees" the latest value, not the value from the time when the query started to fetch.
It depends on useQuery implementation. However I am pretty sure it wont use stale data. I expect following order:
Each time result of useGetData is updated - re-render happens, due to some internal state update
new onSuccess callback (capturing latest getData result in closure) is passed via props to useOtherData and picked up by library to handle next update

React Hooks: Adding new fields to an Object state does not get reflected immediately

I am using React Hooks to manage states within a component.
const addNode = () => {
let pform = pForm
let handles = [vForm, yForm, hForm]
let access_info = [virtualForm, management1Form, management2Form, consoleForm]
let newObj = {
...currentForm,
p: pform,
handles: handles,
access_info: access_info,
}
console.log('newObj', newObj)
setCurrentForm(
newRouterObj
)
console.log(currentForm)
let currArr = [...addedNodes]
currArr.push(currentForm)
setAddedNodes(currArr)
intializeForms()
}
The function above is an onClick that I use when I press an Add button. The forms (pForm, vForm, yForm, etc.) are all separate states. I gather them together and put them into a single object newObj and use setCurrentForm to update the currentForm state to newObj.
When I console.log the newObj, everything goes in fine. However, when I check the currentForm after the setCurrentForm, the fields (p, handles, and access_info) are empty.
I know that states in React can have a delay in updates so I might have to use useEffect. However, in my use case, which is to gather different states and put them in as a new field in the currentForm state seems useEffect is not the best way to solve it. Can anyone help please?
You are misunderstanding exactly how useState works. When you call the useState setter function, the state value isn't actually updated immediately, instead it will trigger the component to re-render with the updated value. Even though you call the setter half way through the function, the state value will remain the original value for the entire lifetime of that function call.
You could slightly tweak what you have to be
const addNode = () => {
...
let currArr = [...addedNodes]
// you know that currentForm is supposed to be newObj, so just push that
// see my explanation above to understand why it currentForm isn't what you expect
currArr.push(newObj)
...
}
It's an async action so values will not be assigned/updated instantly. You need to watch for the changes using useEffect hook to log new values and to do anything in case
useEffect(() => {
// Whenever `currentForm` will be updated, this callback will be invoked
console.log('updated currentForm values', currentForm);
},[currentForm]);

React: scrollIntoView only works inside of setTimeout

My application consists of a basic input where the user types a message. The message is then appended to the bottom of all of the other messages, much like a chat. When I add a new chat message to the array of messages I also want to scroll down to that message.
Each html element has a dynamically created ref based on its index in the loop which prints them out. The code that adds a new message attempts to scroll to the latest message after it has been added.
This code only works if it is placed within a setTimeout function. I cannot understand why this should be.
Code which creates the comments from their array
comments = this.state.item.notes.map((comment, i) => (
<div key={i} ref={i}>
<div className="comment-text">{comment.text}</div>
</div>
));
Button which adds a new comment
<input type="text" value={this.state.textInput} onChange={this.commentChange} />
<div className="submit-button" onClick={() => this.addComment()}>Submit</div>
Add Comment function
addComment = () => {
const value = this.state.textInput;
const comment = {
text: value,
ts: new Date(),
user: 'Test User',
};
const newComments = [...this.state.item.notes, comment];
const newState = { ...this.state.item, notes: newComments };
this.setState({ item: newState });
this.setState({ textInput: '' });
setTimeout(() => {
this.scrollToLatest();
}, 100);
}
scrollToLatest = () => {
const commentIndex = this.state.xrayModalData.notes.length - 1;
this.refs[commentIndex].scrollIntoView({ block: 'end', behavior: 'smooth' });
};
If I do not put the call to scrollToLatest() inside of a setTimeout, it does not work. It doesn't generate errors, it simply does nothing. My thought was that it was trying to run before the state was set fully, but I've tried adding a callback to the setState function to run it, and it also does not work.
Adding a new comment and ref will require another render in the component update lifecycle, and you're attempting to access the ref before it has been rendered (which the setTimeout resolved, kind of). You should endeavor to use the React component lifecycle methods. Try calling your scrollToLatest inside the lifecycle method componentDidUpdate, which is called after the render has been executed.
And while you're certainly correct that setting state is an asynchronous process, the updating lifecycle methods (for example, shouldComponentUpdate, render, and componentDidUpdate) are not initiated until after a state update, and your setState callback may be called before the component is actually updated by render. The React docs can provide some additional clarification on the component lifecycles.
Finally, so that your scroll method is not called on every update (just on the updates that matter), you can implement another lifecycle method, getSnapshotBeforeUpdate, which allows you to compare your previous state and current state, and pass a return value to componentDidUpdate.
getSnapshotBeforeUpdate(prevProps, prevState) {
// If relevant portion or prevState and state not equal return a
// value, else return null
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Check value of snapshot. If null, do not call your scroll
// function
}

React's setState doesn't change state and update component

I'm calling console.log as a callback from setState and it seems like setState doesn't work. I'm trying to write a function which takes state defined like this:
this.state = {
...this.props.substate
};
this.props.substate is an array of objects, so I'm transforming it to object.
Here is a function that transforms state back to array, removes last element and then transforms array into object:
handleClickRem = event => {
event.preventDefault();
console.log(this.state);
const arrayFromState = Object.values(this.state);
console.log(arrayFromState);
const newArray = arrayFromState.slice(0, -1);
console.log('newArray',newArray);
const newState = {...newArray};
console.log(newState);
//newState is exactly what I want it to be
this.setState(newState, () => console.log(this.state));
};
All console logs works perfectly apart from the last one. I'm also checking state in render(), but it also seems to show no change of state.
I've done similarly a function, which adds elements to state and it works perfectly.
I'll be very thankful for help and patience :)

Categories

Resources