Where should we ideally place an api call to be made on occurrence of an event in React
Inside the eventHandler or componentDidUpdate ?
example:
handleItemClick = (item) => (event) => {
this.setState({selectedItem: item});
this.props.requestDataActionDispatch(item);
}
OR
componentDidUpdate(prevProps, prevState, snapshot) {
if(prevState.item !== this.state.item) {
this.props.requestDataActionDispatch(item);
}
}
Depends
But a simple solution is, if you want to call some API after change of state value then you must go for eventHandler. Also check for callback in setState.
handleItemClick = (item) => (event) => {
this.setState({selectedItem: item}, () => this.props.requestData(item));
}
I don't see any reason to wait for component update, I'd just put it in the event handler.
Naturally, in either case your component needs to know how to render appropriately when it has a selected item but doesn't have the data from the API yet...
(Side note: If requestDataActionDispatch results in a state change in your component, you probably want to clear that state when setting the selected item prior to the request, so you don't have one item selected but still have the state related to the previous item. But I'm guessing from the fact it's on props that it doesn't...)
It depends
I would prefer to call the api inside componentDidUpdate. Why ? Because it's cleaner. Whenever there is a change in state or props, componentDidUpdate will be called. So definitely, there has to be a condition inside componentDidUpdate like you have mentioned
if(prevState.item !== this.state.item)
Related
I am having an issue with a Next-js React checkbox list snippet after extracting it into the sandbox.
whenever I clicked the checkbox, I get the error:
TypeError: Cannot read properties of undefined (reading 'id')
which originated from line 264:
setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));
but at the top of the index.js I have defined the static JSON
and in useEffect() I update the pageData state with:
setPageData({
currentPage: threadsDataJSON.threads.current_page,
currentThreads: threadsDataJSON.threads.data,
totalPages: totalPages,
totalThreads: threadsDataJSON.threads.total,
});
so why when I clicked the checkbox it throws the error?
my sandbox link: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js
It looks like your useEffect on line 280 only triggers once you've checked a box (for some reason), so until you trigger that useEffect, pageData.currentThreads remains empty, which is where the error you're running into comes from.
I'd suggest moving all the state initialization from the useEffect into the useState call itself. E.g.
// Bad
const [something, setSomething] = useState(/* fake initial state */);
useEffect(() => {
setSomething(/* real initial state */)
}, []);
// Good
const [something, setSomething] = useState(/* real initial state */);
Here's a fork of your sandbox with this fix.
This is occurring because in Home you've created the handleOnChange function which is passed to the List component that is then passed to the memoized Item component. The Item component is kept the same across renders (and not rerendered) if the below function that you've written returns true:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked
);
}
This means that the Item component holds the first initial version of handleOnChange function that was created when Home first rendered. This version of hanldeOnChange only knows about the initial state of pageData as it has a closure over the initial pageData state, which is not the most up-to-date state value. You can either not memoize your Item component, or you can change your itemPropsAreEqual so that Item is rerendered when your props.handleOnChange changes:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked &&
prevItem.handleOnChange === nextItem.handleOnChange // also rerender if `handleOnChange` changes.
);
}
At this point you're checking every prop passed to Item in the comparison function, so you don't need it anymore and can just use React.memo(Item). However, either changing itemPropsAreEqual alone or removing itemPropsAreEqual from the React.memo() call now defeats the purpose of memoizing your Item component as handleOnChange gets recreated every time Home rerenders (ie: gets called). This means the above check with the new comparison function will always return false, causing Item to rerender each time the parent Home component rerenders. To manage that, you can memoize handleOnChange in the Home component by wrapping it in a useCallback() hook, and passing through the dependencies that it uses:
const handleOnChange = useCallback(
(iindex, id) => {
... your code in handleOnChange function ...
}
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created
This way, a new handleOnChange reference is only created when needed, causing your Item component to rerender to use the new up-to-date handleOnChange function. There is also the useEvent() hook which is an experimental API feature that you could look at using instead of useCallback() (that way Item doesn't need to rerender to deal with handleOnChange), but that isn't available yet as of writing this (you could use it as a custom hook for the time being though by creating a shim or using alternative solutions).
See working example here.
I have a more complex version of the following pseudo-code. It's a React component that, in the render method, tries to get a piece of data it needs to render from a client-side read-through cache layer. If the data is present, it uses it. Otherwise, the caching layer fetches it over an API call and updates the Redux state by firing several actions (which theoretically eventually cause the component to rerender with the new data).
The problem is that for some reason it seems like after dispatching action 1, control flow moves to the top of the render function again (starting a new execution) and only way later continues to dispatch action 2. Then I again go to the top of the render, and after a while I get action 3 dispatched.
I want all the actions to fire before redux handles the rerender of the component. I would have thought dispatching an action updated the store but only forced components to update after the equivalent of a setTimeout (so at the end of the event loop), no? Is it instead the case that when you dispatch an action the component is updated synchronously immediately, before the rest of the function where the dispatch happens is executed?
class MyComponent {
render() {
const someDataINeed = CachingProvider.get(someId);
return (
<div>{someDataINeed == null ? "Loading" : someDataINeed }</div>
);
}
}
class CachingProvider {
get(id) {
if(reduxStoreFieldHasId(id)) {
return storeField[id];
}
store.dispatch(setLoadingStateForId(id));
Api.fetch().then(() => {
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action3);
});
return null;
}
}
In addition to #TrinTragula's very important answer:
This is React behaviour. Things that trigger rerenders that are invoked synchronously from an effect/lifecycle or event handler are batched, but stuff that is invoked asnychronously (see the .then in your code) will trigger a full rerender without any batching on each of those actions.
The same behaviour would apply if you would call this.setState three times in a row.
You can optimize that part by adding batch which is exported from react-redux:
Api.fetch().then(() => {
batch(() => {
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action3);
})
});
You should never invoke heavy operations inside of a render function, since it's going to be triggered way more than you would like to, slowing down your app.
You could for example try to use the useEffect hook, so that your function will be executed only when your id changes.
Example code:
function MyComponent {
useEffect(() => {
// call your method and get the result in your state
}, [someId]);
return (
<div>{someDataINeed == null ? "Loading" : someDataINeed }</div>
);
}
I am creating a game based on an interval. A parent component creates an interval and lets its children register callbacks that are fired each interval.
My setup looks like
<ParentComponent>
<Child1 />
<Child2 />
...
</ParentComponent>
The parent uses React.cloneElement and provides a method to each child
registerCallback( callback, id ) {
const { callbacks } = this.state;
this.setState({
callbacks: callbacks.concat({
callback,
id
})
})
}
This approach worked fine. But now I have two children calling the registerCallback()method from within componentDidMount().
Now only <Child2 /> stores its callback in the parents state.
When I delayed the calling of the registerCallback() method like
setTimeout(() => {
registerCallback(callbackFunction, id)
}, 50 )
it worked fine. But that feels like a very wrong hack, especially since I need to "guess" how long the state needs to update.
So my conclusion was that the state update is just too slow and instead defined an array outside of my component and updated this instead. The strange thing is that if I mutate this array it works fine.
So if I use
array.push({callback, id})
my array looks like [callback1, callback2]
But since I don't want to mutate my array I tried
array.concat({callback, id})
This leaves me with []
While my state looks like [array2]
How can I improve my method within the parent to give children the possibility to call registerCallback() whenever where ever without any problem.
As you've correctly deduced, the problem is setState is asynchronous, so by the time second child calls registerCallback, the state change by first child has not yet propagated to this.state. Try:
registerCallback( callback, id ) {
this.setState(({ callbacks }) => ({
callbacks: callbacks.concat({
callback,
id
})
})
}
You pass a function as first parameter to setState. This function receives the current state (above destructured as { callbacks }) and should return the new state. The function will be invoked either immediately or when a previous state change has propagated, so its state is guaranteed fresh. Thus, you avoid using the potentially stale this.state.
Here's an article about it:
https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
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
}
I have a group of check boxes that filter the data. After the initial data is rendered and as I'm dealing with charts, I want to make the UX dynamic. Therefore, for every state change in my react component, I want to call a function that triggers a service.
handleChange = (query) => {
if(this.state.initialSearchTriggered) {
this.setState({query})
this.triggerReportsService()
}
}
Now the problem is, react takes time to update the state, and the triggerReportsService uses this.state.query to call the service. Therefore, the service query parameter does not have the latest filters. Is there a better way to do this? I was thinking to add componentDidUpdate() method but service calls are getting called multiple times than expected.
componentDidUpdate() {
this.state.initialSearchTriggered ? this.triggerReportsService() : null;
}
Please help
Thank you!
Add a callback to your setState function. The callback will fire when the state update is complete.
handleChange = (query) => {
if(this.state.initialSearchTriggered) {
this.setState({query}, this.triggerReportsService)
}
}