I am trying to use a mousedown event to close an autocomplete when the user clicks outside of the input.
code:
// listen to mouse clicking outside of autocomplete or input
useEffect(() => {
document.addEventListener("mousedown", handleClickOutsideAutocomplete);
return () => {
document.removeEventListener("mousedown", handleClickOutsideAutocomplete);
};
}, []);
const handleClickOutsideAutocomplete = (e) => {
console.log("props:", props);
const { current: wrap } = wrapperRef;
if (wrap && !wrap.contains(e.target)) {
setisOpen(false);
}
};
This code runs as expected. However, when I try to access props on the mousedown event passed via react-redux connect, they are all null. The props passed from the parent component however are present. I have confirmed that on the initial render the react-redux connect props are there as expected.
I thought the mousedown event may be something to do with it so I tested accessing react-redux connect props using a timeout as follows:
useEffect(() => {
const timer = setTimeout(() => {
console.log("The connect props are all null", props);
}, 5000);
return () => clearTimeout(timer);
}, []);
The react-redux connect props here are also null.
Is it possible to access connect props after the initial render, i.e., on a timeout or mousedown event?
Problem is that you haven't added the handleClickOutsideAutocomplete function in the dependency array of the useEffect hook and because of the closure, event handler function doesn't sees the updated value of props.
Solution
Add the handleClickOutsideAutocomplete in the dependency array of the useEffect hook and also wrap handleClickOutsideAutocomplete in the useCallback hook to avoid running the useEffect hook everytime your component re-renders. Also don't forget to list the required dependencies in the dependency array of the useCallback hook.
useEffect(() => {
...
}, [handleClickOutsideAutocomplete]);
const handleClickOutsideAutocomplete = useCallback((e) => {
...
}, [props]);
React recommends to use exhaustive-deps rule as part of their eslint-plugin-react-hooks package. It warns whenever you omit or incorrectly specify a dependency and also suggests a fix.
Related
Suppose I have a component which is like
function Child(props: { onSelect: () => void }) {
...
useEffect(() => {
// want to fire onSelect here
}, [...]);
...
}
Since props.onSelect might change every render (e.g. arrow function), I can't add it to the dependency list of useEffect and call it directly. I used a reducer instead:
const [, dispatch] = useReducer((state: undefined, action: T) => {
props.onSelect(action);
return undefined;
}, undefined);
useEffect(() => {
dispatch(...);
}, [...]);
But now I get the error "Warning: Cannot update a component (Parent) while rendering a different component (Child)."
What's the correct way to fire the parent's onSelect inside some useEffect?
You mention
Since props.onSelect might change every render (e.g. arrow function), I can't add it to the dependency list of useEffect and call it directly
You can, but you should make sure that it does not change if there is no reason.
You should use a useCallback for it on the parent component, so that it remains the same.
function Parent (){
...
const onSelect = useCallback(() => {
// set local state here
}, []);
...
return ... <Child onSelect={onSelect} />
}
If I return a function from useEffect I can be sure that that function will run when a component unmounts. But React seems to wipe the local state before it calls my unmounting function.
Consider this:
function Component () {
const [setting, setSetting] = useState(false)
useEffect(() => {
setSetting(true)
// This should be called when unmounting component
return () => {
console.log('Send setting to server before component is unmounted')
console.log(setting) // false (expecting setting to be true)
}
}, [])
return (
<p>Setting is: {setting ? 'true' : 'false'}</p>
)
}
Can anyone confirm that the expected behaviour is that the components state should be wiped? And, if that is the correct behaviour, how does one go about firing off the current component state to a server just before the component is unmounted?
To give some context, I'm debouncing a post request to a server in order to avoid firing it every time the user changes a setting. The debouncing works nicely, but I need a way to fire the request once a user navigates away from the page, as the queued debouncing method will no longer fire from the unmounted component.
It's not that React "wipes out the state value", it's that you have closure on setting value (the value on-mount).
To get expected behavior you should use a ref and another useEffect to keep it up to date.
function Component() {
const [setting, setSetting] = useState(false);
const settingRef = useRef(setting);
// Keep the value up to date
// Use a ref to sync the value with component's life time
useEffect(() => {
settingRef.current = setting;
}, [setting])
// Execute a callback on unmount.
// No closure on state value.
useEffect(() => {
const setting = settingRef.current;
return () => {
console.log(setting);
};
}, []);
return <p>Setting is: {setting ? "true" : "false"}</p>;
}
I have a react component with a prop that is passed by a redux connect method. There is a useEffect linked specifically to that prop that is supposed to perform an async call when it changes. The problem is the useEffect fires any time I change the redux state anywhere else in that app, despite the prop I have the useEffect attached to not changing.
The useEffect method looks like this
useEffect(() => {
if (userPhoneNumber) {
myAsyncFunction()
.then(() => {
showData()
})
}
}, [userPhoneNumber])
And the userPhoneNumber prop is passed via the react-redux connect method like so:
const mapStateToProps = state => {
return {
userPhoneNumber: state.appState.userPhoneNumber
}
}
export default connect(mapStateToProps)(MyComponent)
From what I understand this could be breaking in two potential places. for one, useEffect should not be firing if the userPhoneNumber prop doesn't change. Also, the mapStateToProps method is not returning a new value so it should not be triggering any sort of a rerender.
The redux state changes that are leading to the unexpected useEffect call are coming from sibling components that have a similar react-redux connect setup to this component, with different state to prop mappings.
When a reducer runs you'll have an entirely new state object, thus a new userPhoneNumber prop reference. You should memoize the userPhoneNumber value. I use reselect to create memoized state selectors, but you could probably use the useMemo hook and use that memoized value in the dependency for the effect.
const memoizedUserPhoneNumber = useMemo(() => userPhoneNumber, [userPhoneNumber]);
useEffect(() => {
if (memoizedUserPhoneNumber) {
myAsyncFunction()
.then(() => {
showData()
})
}
}, [memoizedUserPhoneNumber]);
Using Reselect
import { createSelector } from 'reselect';
const appState = state => state.appState || {};
const userPhoneNumber = createSelector(
appState,
appState => appState.userPhoneNumber;
);
...
const mapStateToProps = state => {
return {
userPhoneNumber: userPhoneNumber(state),
}
}
None of this helps keep the entire functional component from re-rendering when the parent re-renders though, so to help with this you'll need to help "memoize" the props being fed into the component with the memo HOC.
export default memo(connect(mapStateToProps)(MyComponent))
I am trying to use an async function inside a useEffect callback.
There is a behavior i don't understand (in my case related to the throttle function from lodash).
I don't get what is happing, and how to solve it, here is a sample of the code:
import throttle from 'lodash/throttle';
const myRequestWrapped = throttle(myRequest, 300);
const [name, setName] = useState('');
useEffect(() => {
myRequest(name) // No warning
myRequestWrapped(name); // React Hook useEffect has a missing dependency: 'myRequestWrapped'. Either include it or remove the dependency array
}, [ name ]);
If i add myRequestWrapped as a dependency, i have an infinite loop (the effect is triggered continuously).
I guess the throttle method works with a timer and it returns a different result at every run so i can understand why the infinite loop.
But i really don't understand why React wants it as a dependency (especially that it works without adding it !).
What is the logic?
Why myRequestWrapped and not myRequest?
Should i ignore the warning or do you know a clean way to solve that?
Thanks.
Its not React that wants you to add myRequestWrapped as a dependency but its eslint.
Also you must note that ESLint isn't aware of the programmers intention so it just warns the user if there is a scope of error being made.
Hooks heavily rely on closures and sometimes its difficult to figure out bugs related to closures and that is why eslint prompts if there is a case of a fucntion of variabled used within useEffect that might reflect the updated values.
Of course the check isn't perfect and you could carefully decide whether you need to add a dependency to useEffect or not.
If you see that what you wrote is perfectly correct. You can disable the warning
useEffect(() => {
myRequest(name);
myRequestWrapped(name);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ name ]);
Also you must not that throttle function cannot be used within render of functional componentDirectly as it won't be effective if it sets state as the reference of it will change
The solution here is to use useCallback hook. Post that even if you add myRequestWrapped as a dependency to useEffect you won't be seeing an infinite loop since the function will only be created once as useCallback will memoize and return the same reference of the function on each render.
But again you must be careful about adding dependency to useCallback
import throttle from 'lodash/throttle';
const Comp = () => {
const myRequestWrapped = useCallback(throttle(myRequest, 300), []);
const [name, setName] = useState('');
useEffect(() => {
myRequest(name);
myRequestWrapped(name);
}, [ name ]);
...
}
Shubham is right: it's not REACT, but ESLint instead (or TSLint, depending on the Linter you are using).
If I may add, the reason why the Linter suggest you to add myRequestWrapped is because of how the closures work in JavaScript.
To let you understand, take this easier example (and I need this because I would need to know what it's inside myRequestWrapped:
const MyComp = props => {
const [count, setCount] = React.useState(0);
const handleEvent = (e) => {
console.log(count);
}
React.useEffect(() => {
document.body.addEventListener('click', handleEvent);
return () => { document.body.removeEventListener('click', handleEvent); }
}, []);
return <button onClick={e => setCount(s => s +1)}>Click me</button>
}
So, right now, when MyComp is mounted, the event listener is added to the document.body, and anytime the click event is triggered, you call handleEvent, which will log count.
But, since the event listener is added when the component is mounted, the variable count inside handleEvent is, and always will be, equal to 0: that is because the instance of handleEvent created when you added the event listener is just one, the one that you associated with the event listener.
Instead, if you would write the useEffect like this:
React.useEffect(() => {
document.body.addEventListener('click', handleEvent);
return () => { document.body.removeEventListener('click', handleEvent); }
}, [handleEevent]);
Anytime the handleEvent method is updated, also your event listener is updated with the one handleEvent, thus when clicking on the document.body you will always log the latest count value.
I have a context that is used to show a full page spinner while my application is performing long running tasks.
When I attempt to access it inside useEffect I get a the react-hooks/exhaustive-deps ESLint message. For example the following code, although it works as expected, states that busyIndicator is a missing dependency:
const busyIndicator = useContext(GlobalBusyIndicatorContext);
useEffect(() => {
busyIndicator.show('Please wait...');
}, []);
This article suggests that I could wrap the function with useCallback which might look as follows:
const busyIndicator = useContext(GlobalBusyIndicatorContext);
const showBusyIndicator = useCallback(() => busyIndicator.show('Please wait...'), []);
useEffect(() => {
showBusyIndicator();
}, [showBusyIndicator]);
Although this works it has moved the issue to the useCallback line which now complains about the missing dependency.
Is it ok to ignore the ESLint message in this scenario or am I missing the something?
If your busyIndicator is not changed during the life of the component, you could simply add it as a dependency to useEffect:
const busyIndicator = useContext(GlobalBusyIndicatorContext);
useEffect(() => {
busyIndicator.show('Please wait...');
}, [busyIndicator]);
If busyIndicator could be changed and you don't want to see an error, then you could use useRef hook:
const busyIndicator = useRef(useContext(GlobalBusyIndicatorContext));
useEffect(() => {
busyIndicator.current.show('Please wait...');
}, []);
The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. read more
No need to Wrap your useContext in useRef Hook.
you can update your context data just call in useEffect Brackets
like this.
const comingData = useContext(taskData);
useEffect(() => {
console.log("Hi useEffect");
}},[comingData]); //context data is updating here before render the component