useEffect with dependencies should run its cleanup after the render, so it's actually proper behavior.
To run cleanup on unmount, use useEffect with an empty dependencies array
React.useEffect(() => {
// Side effect where we modify dom
console.log('modifying: ', name);
const element = document.createElement('span');
element.innerText = `This text was added as a side-effect for component: ${name}`;
const container = document.getElementById(name);
container?.appendChild(element);
// Clean-up on unmount
return () => {
console.log('unmounting: ', name);
const container = document.getElementById(name);
container?.parentNode?.removeChild(container);
};
}, []);
But if you want to achieve same (invalid) behavior as with react-router v5, you have to force router to unnecessarily re-mount same component every time its prop is changed.
This is not recommended but here you go:
const pages = [...Array(3).keys()].map((x) => (
<Route
key={`route-${x}`}
path={`component-${x}`}
element={<Component key={`component-${x}`} name={`component-${x}`} />}
/>
));
(unique key prop will force react to not reuse same component)
Explanation to useEffect:
useEffect always runs after a render
- previous
useEffect is always cleaned up before running new useEffect
So the first is render, then useEffect with new dependency value wants to run, so the previous useEffect with old value must be cleaned up.
Example
interface FooProps {
id: string
}
const Foo: React.FC<FooProps> = ({ id }) => {
React.useEffect(() => {
console.log({ id })
return () => {
console.log({ id })
}
}, [id]);
return <div id={id} />
}
Order of events when this component is used:
id starts as 'a'
- div with id
'a' is rendered
useEffect runs with its dependency id set to 'a'
- Foo receives property
id with value changed to 'b'
- div with id
'b' is rendered instead of div with id 'a'
useEffect runs cleanup with its dependency id set to 'a'
useEffect runs with dependency id set to 'b'
So to make this absolutely clear:
useEffect(() => {
return () => {
// This cleanup runs only on unmount
}
}, []);
useEffect(() => {
return () => {
// This cleanup runs right before this useEffect runs with updated name value
}
}, [name]);