Best Practice to toggle Childcomponent from Parent - reactjs

I created a toast notification in React which is Toggleable via the timeNotification function. For testing purposes, I used a button in the same Component to toggle the notification. All the notification does is to set a CSS class to "active" and after 5 seconds remove it.
const timeNotification = () => {
setShowNotification(true);
setShowProgressBar(true)
setTimeout(() => {
setShowNotification(false)
}, 5000);
setTimeout(() => {
setShowProgressBar(false)
}, 5300)
}
my goal is to make this Component triggerable in my React app whenever I need it. For example, when I require the user to log in to my website and the backend server is not reachable it is supposed to use my Notification component to send an error to the user.
What is the best practice to do so?
The first idea was to simply pass a function from the child to the parent component which is a bad practice because as far as I know functionality should always be passed down and never upwards.
The second idea was to toggle the component via useEffect
useEffect(()=> {
timeNotification()
}, [])
This approach only works once. But after the Component is rendered for the first time, it just vanishes (gets moved out of the user's view). This approach would work if Id make the component un render after 5 seconds and rerender it as soon as a new error occurs which is also a bad practice.

I just found a solution, I still don't know if it's the best practice however it works. I did use the useEffect hook in the Child component.
useEffect(() => {
timeNotification();
}, [props.refresh])
its triggered by the Parent via props and useState
const [showLoginError, setShowLoginError] = useState<number>(0);
setShowLoginError(prev => prev + 1)
when ever the parent increases the showLoginError the Notification useEffect gets triggered

Related

How to implement promise when updating a state in functional React (when using useState hooks)

I know similar questions are bouncing around the web for quite some time but I still struggle to find a decision for my case.
Now I use functional React with hooks. What I need in this case is to set a state and AFTER the state was set THEN to start the next block of code, maybe like React with classes works:
this.setState({
someStateFlag: true
}, () => { // then:
this.someMethod(); // start this method AFTER someStateFlag was updated
});
Here I have created a playground sandbox that demonstrates the issue:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-6zss6q?file=/demo.tsx
Please push the button to get the confirmation dialog opened. Then confirm with "YES!" and notice the lag. This lag occurs because the loading data method starts before the close dialog flag in state was updated.
const fireTask = () => {
setOpen(false); // async
setResult(fetchHugeData()); // starts immediately
};
What I need to achieve is maybe something like using a promise:
const fireTask = () => {
setOpen(false).then(() => {
setResult(fetchHugeData());
});
};
Because the order in my case is important. I need to have dialog closed first (to avoid the lag) and then get the method fired.
And by the way, what would be your approach to implement a loading effect with MUI Backdrop and CircularProgress in this app?
The this.setState callback alternative for React hooks is basically the useEffect hook.
It is a "built-in" React hook which accepts a callback as it's first parameter and then runs it every time the value of any of it's dependencies changes.
The second argument for the hook is the array of dependencies.
Example:
import { useEffect } from 'react';
const fireTask = () => {
setOpen(false);
};
useEffect(() => {
if (open) {
return;
}
setResult(fetchHugeData());
}, [open]);
In other words, setResult would run every time the value of open changes,
and only after it has finished changing and a render has occurred.
We use a simple if statement to allow our code to run only when open is false.
Check the documentation for more info.
Here is how I managed to resolve the problem with additional dependency in state:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-gevciu?file=/demo.tsx
Thanks to all that helped.

React unnecessary render of element

I've just started learning React and was putting together a small app which makes calls to a quotes API. The API has an endpoint that returns a random quote. When the app initially loads it makes a call to the API and shows a quote, and there's a button that can be clicked to get a new random quote (new call to the API).
I have a root component named App. This component has a QuoteWrap component as a child. The QuoteWrap component has two children: the button that is used to get a new random quote and a Quote component which shows the author of the quote and the quote itself. This is the code inside of the QuoteWrap component:
export default function QuoteWrap() {
const { quoteData, isLoading, fireNewCall } = useQuote();
const handleClick = () => {
fireNewCall();
};
return(
<>
<button onClick={handleClick}>Get random quote</button>
{ isLoading ?
<h2>Loading...</h2>
:
<Quote author={quoteData.author} quote={quoteData.quote} />
}
</>
);
}
useQuote() is a custom hook that manages the calls to the API and returns 3 values: 1- the data, 2- if a call is in process and 3- a function to make a call to the API.
Obviously, every time the button is clicked, the whole QuoteWrap component is re-rendered (as quoteData and isLoading change). But really, the button doesn't need to be re-rendered as it never changes.
So I thought: ok, I can move the button up to the App component. But then I don't have access to the fireNewCall function in the useQuote hook.
How can I prevent the button from being re-rendered? Is it even important in this case or am I getting too obsessed with React re-renders?
Thanks!
Your component will re-render every time the handleClick function changes, which is every time that QuoteWrap is rendered.
The solution is the useCallback hook. useCallback will return the same function to handleClick, every time QuoteWrap is rendered, so long as the dependencies haven't changed.
https://reactjs.org/docs/hooks-reference.html#usecallback
You would use it like this:
const handleClick = useCallback(() => {
fireNewCall();
},[fireNewCall]);
fireNewCall is the dependency, so as long as useQuote returns a stable fireNewCall function, then your button will not re-render, since the handleClick property hasn't changed.
I think you might get too obsessed with React re-renders. The button should be re-rendered because, handleClick should be changed when fireNewCall changed for some case. Even if, handleClick will never be changed. It's no need to think about an element re-render.
Pretty much what Benjamin and Viet said - in your original code, a new function is assigned to handleClick on each render. You can use React.useCallback to maintain the original function reference and only update it when something in the dependency array changes - in this case, just fireNewCall needs to go into the dependency array.
But as Viet says, don't get too obsessed with it. Using React.useCallback might even slow down your code. Check out Kent C. Dodds When to useMemo and useCallback post for more insight.

How to force refresh of GrandParent component from grandchild component with React hooks in SPFx

In the main component of a SPFx web part, I have a data load running in a React.useEffect
const [jobsGets, setJobsGets] = React.useState(0);
React.useEffect(() => {
{Some code to get data from SharePoint}
});
}, [jobsGets]);
So a refresh will happen when the value of "jobsGets" will change, right?
This component calls another component that calls another and the last has an event that should refresh by changing "jobsGets".
If I write a fanction and pass it down the props of each generation's component,
It ends running in the main component forever. So this is not a good solution.
How do I setJobsGets(jobsGets+1); from the grandchild components?
Any hint is welcome.
Thank you
you are likely updating jobs at grandChild with useEffect, that runs on mount and every update. if you pass a second empty array [] param, this update will run only after first render if that's your specific case, avoiding retriggers:
// at some granchild
useEffect(() => { ...logic here }, [])
if useEffect should run after on specific param's update, you pass that param as argument to the array. useEffect will run only on changes for the params dependencies set on [].
useEffect(() => { ...logic here }, [param])
you may also look at useContext for passing down state better at deep tree.

Save React Component scroll position in Redux on Component update

I have a React Component using a hook to save the scroll position of the component when the component unmounts. This works great but fails when navigating from one set of data to another set of data without the component unmounting.
For instance, imagine the Slack Interface where there is a sidebar of message channels on the left and on the right is a list of messages (messageList). If you were to navigate between two channels, the messageList component would update with a new set of data for the messageList, but the component was never unmounted so scroll position never gets saved.
I came up with a solution that works, but also throws a warning.
My current useEffect hook for the component (stripped down) and the code that currently saves scroll position whenever the messageList ID changes:
// Component...
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Save scroll position when Component unmounts
useEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, []);
// Save scroll position when Parent ID changes
const oldParent = usePrevious(parent);
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
// ...Component
The error this throws is:
Warning: Cannot update a component from inside the function body of a different component.
And the line that is causing it is the setScrollOffset call inside of the last if block. I'm assuming that while this works it is not the way that I should be handling this sort of thing. What is a better way to handle saving scroll position when a specific prop on the component changes?
Add parent._id to the dependency array. Refactor your code to still cache the previous parent id, add that to the dependency, and move the conditional test inside the effect.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect.
// Return previous parent id and cache current
const oldParent = usePrevious(parent);
// Save scroll position when Component unmounts or parent id changes
useEffect(() => {
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id, oldParent]);
If this does't quite fit the bill, then use two effects, one for the mount/unmount and the other for just updates on the parent id.
Thanks to the suggestions of #drew-reese, he got me pointed down the right path. After adopting his solution (which previously I could not get working properly), I was able to isolate my problem to usage with react-router. (connected-react-router in my case). The issue was that the component was rendering and firing the onScroll event handler and overwriting my scroll position before I could read it.
For me the solution ended up being to keep my existing useEffect hook but pull the scroll offset save out of it and into useLayoutEffect (Had to keep useEffect since there is other stuff in useEffect that I removed for the sake of keeping the sample code above lean). useLayoutEffect allowed me to read the current scroll position before the component fired the onScroll event which was ultimately overwriting my saved scroll position reference to 0.
This actually made my code much cleaner overall by removing the need for my usePrevious hook entirely. My useLayoutEffect hook now looks like this:
useLayoutEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id]);

Can't perform a React State update on unMounted child component?

Am getting this warning:
Can't perform a React state update on unmounted component. This is a no-op...
It results from a child component and I can't figure out how to make it go away.
Please note that I have read many other posts about why this happens, and understand the basic issue. However, most solutions suggest cancelling subscriptions in a componentWillUnmount style function (I'm using react hooks)
I don't know if this points to some larger fundamental misunderstanding I have of React,but here is essentially what i have:
import React, { useEffect, useRef } from 'react';
import Picker from 'emoji-picker-react';
const MyTextarea = (props) => {
const onClick = (event, emojiObject) => {
//do stuff...
}
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
});
useEffect(() => {
return () => {
console.log('will unmount');
isMountedRef.current = false;
}
});
return (
<div>
<textarea></textarea>
{ isMountedRef.current ? (
<Picker onEmojiClick={onClick}/>
):null
}
</div>
);
};
export default MyTextarea;
(tl;dr) Please note:
MyTextarea component has a parent component which is only rendered on a certain route.
Theres also a Menu component that once clicked, changes the route and depending on the situation will either show MyTextarea's parent component or show another component.
This warning happens once I click the Menu to switch off MyTextarea's parent component.
More Context
Other answers on StackOverflow suggest making changes to prevent state updates when a component isn't mounted. In my situation, I cannot do that because I didn't design the Picker component (rendered by MyTextarea). The Warning originates from this <Picker onEmojiClick={onClick}> line but I wouldn't want to modify this off-the-shelf component.
That's explains my attempt to either render the component or not based on the isMountedRef. However this doesn't work either. What happens is the component is either rendered if i set useRef(true), or it's never rendered at all if i set useRef(null) as many have suggested.
I'm not exactly sure what your problem actually is (is it that you can't get rid of the warning or that the <Picker> is either always rendering or never is), but I'll try to address all the problems I see.
Firstly, you shouldn't need to conditionally render the <Picker> depending on whether MyTextArea is mounted or not. Since components only render after mounting, the <Picker> will never render if the component it's in hasn't mounted.
That being said, if you still want to keep track of when the component is mounted, I'd suggest not using hooks, and using componentDidMount and componentWillUnmount with setState() instead. Not only will this make it easier to understand your component's lifecycle, but there are also some problems with the way you're using hooks.
Right now, your useRef(true) will set isMountedRef.current to true when the component is initialized, so it will be true even before its mounted. useRef() is not the same as componentDidMount().
Using 'useEffect()' to switch isMountedRef.current to true when the component is mounted won't work either. While it will fire when the component is mounted, useEffect() is for side effects, not state updates, so it doesn't trigger a re-render, which is why the component never renders when you set useRef(null).
Also, your useEffect() hook will fire every time your component updates, not just when it mounts, and your clean up function (the function being returned) will also fire on every update, not just when it unmounts. So on every update, isMountedRef.current will switch from true to false and back to true. However, none of this matters because the component won't re-render anyways (as explained above).
If you really do need to use useEffect() then you should combine it into one function and use it to update state so that it triggers a re-render:
const [isMounted, setIsMounted] = useState(false); // Create state variables
useEffect(() => {
setIsMounted(true); // The effect and clean up are in one function
return () => {
console.log('will unmount');
setIsMounted(false);
}
}, [] // This prevents firing on every update, w/o it you'll get an infinite loop
);
Lastly, from the code you shared, your component couldn't be causing the warning because there are no state updates anywhere in your code. You should check the picker's repo for issues.
Edit: Seems the warning is caused by your Picker package and there's already an issue for it https://github.com/ealush/emoji-picker-react/issues/142

Resources