React: add body class only to certain components in useEffect hook? - reactjs

I am using React 16.8.6 and hooks. I am new to hooks.
I have a component loaded in a route I need to add a body class to. When the user leaves this page, I need the class removed. I am using
useEffect(() => {
document.body.className = 'signin';
}, []);
This correctly adds the class to the body tag. Except when I navigate to another page, the class remains. If I reload the second page it's gone.
How do I remove the class when the component unmounts when the route changes?

If your effect returns a function, it will act as a cleanup.
useEffect(() => {
document.body.classList.add('signin');
return function cleanup() {
document.body.classList.remove('signin');
};
}, []);
You can check out Effects with cleanup in the documentation

The useEffect hook supports a cleanup function that runs when the component unmounts
useEffect(() => {
document.body.className = 'signin';
return () => { document.body.className = ''; }
});
See the docs.

Related

How to start animation before component is removed in React?

I would like to know how do I trigger an animation before removing a functional React component.
I am using the npm package to manage animations via JS. So I just need to call the function. For example function animate.
import React from "react";
useEffect(() => {
function animateStart() {
console.log('AnimateStart')
}
},[]);
export default function () {
return(
<div className={'component'}/>
)
}
This is how I am triggering the animation when the component appears. I would like to somehow catch the removal and postpone it for the duration of the animation.
At the moment I'm calling the delete animation from the parent component. This forces me to store the animateExit function in another component. It is not comfortable :(
Try this:
useEffect(() => {
function animateStart() {
console.log('AnimateStart')
}
return () => {/* whatever you want to do, it will be called everytime when this component is unmounted*/}
},[]);

useEffect of children component called before useEffect of parent

I am trying to understand why the useEffect of a children component gets called before the Parent component useEffect.
From my understanding, useEffect shoulde be called in the order they are defined based on React's documentation:
React will apply every effect used by the component, in the order they were specified.
This would mean, a Parent's useEffect should be called before a Children's useEffect, but this is not the case.
Example:
const MainComponent = () => {
return {
<ParentComponent />
}
const ParentComponent = () => {
useEffect(() => {
console.log('parent');
}, []);
return <div>Parent <ChildrenComponent /></div>;
}
const ChildrenComponent = () => {
useEffect(() => {
console.log('children');
}, []);
return <div>Children</div>;
}
If you check the console, you should see first children and then parent
Live Code: https://codesandbox.io/s/crazy-butterfly-yn046?file=/src/App.js
My gut tells me this has to do with how react does Layout and Paint of the Parent-Children components?
This:
React will apply every effect used by the component, in the order they were specified.
Would be more precisely stated as:
React will apply every effect used by the component, in the order they were specified in that component.
For example:
const SomeComponent = () => {
useEffect(() => {
console.log('This will run first');
});
useEffect(() => {
console.log('This will run second');
});
// ...
is guaranteed to run in order.
It's not saying anything about the order that effects in different components run.

How to access redux connect mapStateToProps without a rerender

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.

Know when a user leaves component - React

I'm building an app that uses React, Redux, Firebase and the function onSnapshot(). I read that to lower costs and updates to the server is good practice to unsubscribe the onSnapshot when the user leaves the component.
So my question is: How can I know when a user leaves a component or a path in react to unsuscribe the onSnapshot?
Thanks!
In a React class component, use componentDidMount and componentWillUnmount to add and remove listeners or subscriptions:
class MyComponent extends React.Component {
componentDidMount() {
// Add listeners, subscriptions, etc.
}
componentWillUnmount() {
// Remove listeners, subscriptions, etc.
}
render() {
...
}
}
In a React function component, use the useEffect hook to do the same. Be sure to add the proper dependencies in useEffect's dependency array (second argument).
const MyComponent = (props) => {
useEffect(() => {
// Add listeners, subscriptions, etc.
return () => {
// This function will be called just before the component unmounts.
// Remove listeners, subscriptions, etc.
};
}, [/* Dependency array */]);
return (...);
};
componentWillUnmount() { // your Code here }
"This method is called when a component is being removed from the DOM" (from: https://reactjs.org/docs/react-component.html). Hope that is what you were looking for.

Is it possible to use a custom hook inside useEffect in React?

I have a very basic custom hook that takes in a path an returns a document from firebase
import React, { useState, useEffect, useContext } from 'react';
import { FirebaseContext } from '../sharedComponents/Firebase';
function useGetDocument(path) {
const firebase = useContext(FirebaseContext)
const [document, setDocument] = useState(null)
useEffect(() => {
const getDocument = async () => {
let snapshot = await firebase.db.doc(path).get()
let document = snapshot.data()
document.id = snapshot.id
setDocument(document)
}
getDocument()
}, []);
return document
}
export default useGetDocument
Then I use useEffect as a componentDidMount/constructor to update the state
useEffect(() => {
const init = async () => {
let docSnapshot = await useGetDocument("products/" + products[selectedProduct].id + "labels/list")
if(docSnapshot) {
let tempArray = []
for (const [key, value] of Object.entries(docSnapshot.list)) {
tempArray.push({id: key, color: value.color, description: value.description})
}
setLabels(tempArray)
} else {
setLabels([])
}
await props.finishLoading()
await setLoading(false)
}
init()
}, [])
However, I get an Invariant Violation from "throwInvalidHookError" which means that I am breaking the rules of hooks, so my question is whether you can't use custom hooks inside useEffect, or if I am doing something else wrong.
As far as I know, the hooks in a component should always be in the same order. And since the useEffect happens sometimes and not every render that does break the rules of hooks. It looks to me like your useGetDocument has no real need.
I propose the following solution:
Keep your useGetDocument the same.
Change your component to have a useEffect that has the document as a dependency.
Your component could look like the following:
const Component = (props) => {
// Your document will either be null (according to your custom hook) or the document once it has fetched the data.
const document = useGetDocument("products/" + products[selectedProduct].id + "labels/list");
useEffect(() => {
if (document && document !== null) {
// Do your initialization things now that you have the document.
}
}, [ document ]);
return (...)
}
You can't use a hook inside another hook because it breaks the rule Call Hooks from React function components and the function you pass to useEffect is a regular javascript function.
What you can do is call a hook inside another custom hook.
What you need to do is call useGetDocument inside the component and pass the result in the useEffect dependency array.
let docSnapshot = await useGetDocument("products/" + products[selectedProduct].id + "labels/list")
useEffect(() => { ... }, [docSnapshot])
This way, when docSnapshot changes, your useEffect is called.
Of course you can call hooks in other hooks.
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
But...
You are not using a hook inside another hook.
You realise that you what you pass to useEffect is a callback, hence you are using your custom hook inside the body of the callback and not the hook (useEffect).
If you happen to use ESLint and the react-hooks plugin, it'll warn you:
ESLint: React Hook "useAttachDocumentToProspectMutation" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.(react-hooks/rules-of-hooks)
That being said, you don't need a useEffect at all. And useGetDocument doesn't return a promise but a document.
When calling your hook
const document = useGetDocument("products/" + products[selectedProduct].id + "labels/list");
It will return undefined the first time around, then the document for the subsequent renders as per #ApplePearPerson's answer.

Resources