Does this "custom react hook" breaks the hooks law? - reactjs

I am using many useCallbacks in useEffects in the same format syntax and feel like shortening them. So, I map them in this hook. effects is an array of useCallback functions.
import React from 'react';
const useEffects = (effects: Function[]) =>
effects.map((effect) =>
React.useEffect(() => {
effect();
}, [effect])
);
export default useEffects;

Hooks can't be defined inside an array.
I wrote an article recently about the rendering order of hooks. https://windmaomao.medium.com/understanding-hooks-part-4-hook-c7a8c7185f4e
It really goes down to below snippet how Hooks is defined.
function hook(Hook, ...args) {
let id = notify()
let hook = hooks.get(id)
if(!hook) {
hook = new Hook(id, element, ...args)
hooks.set(id, hook)
}
return hook.update(...args)
}
When a hook is registered, it requires an unique id, as in line notify(). Which is just a plain i++ placed inside the Component where the hook is written inside.
So if you have a fixed physical location of the hook, you have a fixed id. Otherwise the id can be random, and since the render Component function is called every render cycle, a random id is not going to find the right hook in the next render cycle.
That is why if can not be written before the hook statement either. Just try
const Component = () => {
const b = true
if (!b) return null
const [a] = useState(1)
}
You should get similar error.

Related

Which is the correct way to handle values props before charge component in React?

i have a doubt about React hook, his props and useEffect.
In my component, i receive a list inside props, i must filter the received list and get a value from the list before the component is mounted.
(this code is a simplified example, i apply another filters and conditions more complex)
function MyComponent(props) {
const [value, setValue] = useState(null)
useEffect(()=>{
var found = props.myList.find((x)=> {return x.name=="TEST"});
if(found.length > 0)
setValue("Hello World")
}, []);
return (
<div>{value}</div>
)
}
Is correct to use useEffect for get and set a value respect to props before the component is mounted?
This useEffect is executed after the 1st render, so null would be "rendered" first, and then the useEffect would update the state causing another render.
However, if your value is dependent on the props, and maybe on other states (filters for example), this is a derived value, and it doesn't need to be used as a state. Instead calculate it on each render, and if it's a heavy computation that won't change on every render wrap it with useMemo:
function MyComponent({ myList }) {
const value = useMemo(() => {
const found = myList.some(x => x.name=="TEST");
return found ? 'Hello World' : '';
}, [myList]);
return (
<div>{value}</div>
)
}
There's no need to use useEffect here, you can simply pass a callback to the useState hook. The callback is only ran on the initialization of the hook so this has the same outcome as your useEffect hook (i.e. it doesn't run on every rerender so same performance hit).
function MyComponent({ myList }) {
const [value, setValue] = useState(() => {
const found = myList.find(x => x.name === 'TEST');
return found ? 'Hello World' : null;
});
return (
<div>{value}</div>
);
}
No need for any hook at all, actually, you should just write it like this:
function MyComponent({ myList }) {
const found = myList.some(x => x.name == "TEST");
const value = found ? 'Hello World' : '';
return (
<div>{value}</div>
)
}
Using useEffect or even only useState is conceptually wrong. The useState hook is used when a component needs to have its own independent internal state which isn't the case here, as the value is purely derived from the props. The useEffect hook is an escape hatch typically used when you need to sync with some external imperative API, not at all the case here.
The solution with useMemo is perfectly fine, and more efficient if your filtering condition is very computationally expensive, but it is also a lot more complicated to understand and to maintain, so I would suggest going for the simplest solution until the lack of memoization actually becomes a measurable problem.

React with TypeScript - React has detected a change in the order of Hooks called by ComponentName

I am working with a project with users. Right now I am working with the UserProfile
This is the error I am recieving.
React has detected a change in the order of Hooks called by UserProfile
Previous render Next render
------------------------------------------------------
1. useState useState
2. useContext useContext
3. useEffect useEffect
4. undefined useContext
Let me show some code of the UserProfile component.
export const UserProfile = () => {
document.title = `${title} - My Profile`;
const [profile, setProfile] = useState<UserDetails>();
const {claims} = useContext(AuthContext);
const getUserEmail = (): string => {
return claims.filter(x => x.name === "email")[0]?.value.toString();
}
useEffect(() => {
axios.get(`${urlAuth}?userName=${getUserEmail()}`)
.then((response: AxiosResponse<UserDetails>) => {
setProfile(response.data);
})
}, [getUserEmail]);
return (
profile ?
<article>
<h1>This profile belongs to {UserName()}</h1>
<h2>{profile.name}</h2>
</article>
: <div>Loading...</div>
)
}
I get a warning at the getUserEmail function,
It says
The 'getUserEmail' function makes the dependencies of useEffect Hook (at line 26) change on every render.
Move it inside the useEffect callback.
Alternatively, wrap the definition of 'getUserEmail' in its own useCallback() Hook.
I am not sure on how this should be done.
Any ideas on what I could do?
Thanks
Wrap getUserEmail's value in a useCallback.
On every render, getUserEmail essentially becomes a 'new' function.
When there's a function in the deps array of a useEffect or other such hooks, React checks it by reference. Since each component function execution/rerender leads to the creation of a new function, your useEffect hook will actually run every single time, sending you into a re-render loop (because it'll run the useEffect, update the state with setProfile, which in turn will trigger another execution, where getUserEmail is different again, leading to the useEffect to run again and so on).
const getUserEmail = useCallback((): string => {
return claims.filter(x => x.name === "email")[0]?.value.toString();
}, [claims]);
This should give you a memoized callback that will only be recreated if claims changes. Since claims comes from your context, this should be safe as a dependency.
The reason why you are getting error about order of hooks is I think because of this:
profile ?
<article>
<h1>This profile belongs to {UserName()}</h1>
<h2>{profile.name}</h2>
</article>
: <div>Loading...</div>
If UserName is a component you should not call it as function, rather as element <UserName/>. When you call it as function react thinks some of the hooks which you call inside it belong to the parent component - this combined with condition profile ? could give you the error.

How to conditionally render a React component that uses hooks

The following (albeit contrived) component violates react-hooks/rules-of-hooks (via eslint)
function Apple(props){
const {seeds} = props;
if(!seeds){
return null;
}
const [bitesTaken, setBitesTaken] = useState(0);
return <div>{bitesTaken}</div>
}
with the following error
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
I understand that hooks need to be called in the same order, but this kind of conditional rendering (so long as no hooks are invoked before the return, does not prevent the hooks from being called in the same order. I think the eslint rule is too strict, but maybe there's a better way.
What is the best way to conditionally render a component which uses hooks?
Another option is to split the component.
import React,{useState} from 'react';
const Apple = (props) => {
const {seeds} = props;
return !!seeds && <AppleWithSeeds />;
};
const AppleWithSeeds =(props) => {
const [bitesTaken] = useState(0);
return <div>{bitesTaken}</div>
};
export default Apple;
Advantage of this method is that your components stay small and logical.
And in your case you might have something more than '0' in the useState initializer which you don't want unnecessary at the top before the condition.
You can't conditionally use hooks, so move the hook above the condition.
function Apple(props){
const {seeds} = props;
const [bitesTaken, setBitesTaken] = useState(0);
if(!seeds){
return null;
}
return <div>{bitesTaken}</div>
}
You can also simplify the rendering like this:
function Apple(props) {
const { seeds } = props
const [bitesTaken, setBitesTaken] = useState(0)
return !!seeds && <div>{bitesTaken}</div>
}
If seeds is falsey, then false will be returned (which renders as nothing), otherwise, the div will be returned.
I've added the double exclamation point to seeds to turn it into a boolean, because if seeds is undefined then nothing is returned from render and React throws an error.

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.

More advanced comparison in React's useEffect

I am looking for a way to perform more advanced comparison instead of the second parameter of the useEffect React hook.
Specifically, I am looking for something more like this:
useEffect(
() => doSomething(),
[myInstance],
(prev, curr) => { /* compare prev[0].value with curr[0].value */ }
);
Is there anything I missed from the React docs about this or is there any way of implementing such a hook on top of what already exists, please?
If there is a way to implement this, this is how it would work: the second parameter is an array of dependencies, just like the useEffect hook coming from React, and the third is a callback with two parameters: the array of dependencies at the previous render, and the array of dependencies at the current render.
you could use React.memo function:
const areEqual = (prevProps, nextProps) => {
return (prevProps.title === nextProps.title)
};
export default React.memo(Component, areEqual);
or use custom hooks for that:
How to compare oldValues and newValues on React Hooks useEffect?
In class based components was easy to perform a deep comparison. componentDidUpdate provides a snapshot of previous props and previous state
componentDidUpdate(prevProps, prevState, snapshot){
if(prevProps.foo !== props.foo){ /* ... */ }
}
The problem is useEffect it's not exactly like componentDidUpdate. Consider the following
useEffect(() =>{
/* action() */
},[props])
The only thing you can assert about the current props when action() gets called is that it changed (shallow comparison asserts to false). You cannot have a snapshot of prevProps cause hooks are like regular functions, there aren't part of a lifecycle (and don't have an instance) which ensures synchronicity (and inject arguments). Actually the only thing ensuring hooks value integrity is the order of execution.
Alternatives to usePrevious
Before updating check if the values are equal
const Component = props =>{
const [foo, setFoo] = useState('bar')
const updateFoo = val => foo === val ? null : setFoo(val)
}
This can be useful in some situations when you need to ensure an effect to run only once(not useful in your use case).
useMemo:
If you want to perform comparison to prevent unecessary render calls (shoudComponentUpdate), then useMemo is the way to go
export React.useMemo(Component, (prev, next) => true)
But when you need to get access to the previous value inside an already running effect there is no alternatives left. Cause if you already are inside useEffect it means that the dependency it's already updated (current render).
Why usePrevious works
useRef isn't just for refs, it's a very straightforward way to mutate values without triggering a re render. So the cycle is the following
Component gets mounted
usePrevious stores the inital value inside current
props changes triggering a re render inside Component
useEffect is called
usePrevious is called
Notice that the usePrevious is always called after the useEffect (remember, order matters!). So everytime you are inside an useEffect the current value of useRef will always be one render behind.
const usePrevious = value =>{
const ref = useRef()
useEffect(() => ref.current = value,[value])
}
const Component = props =>{
const { A } = props
useEffect(() =>{
console.log('runs first')
},[A])
//updates after the effect to store the current value (which will be the previous on next render
const previous = usePrevious(props)
}
I hit the same problem recently and a solution that worked for me is to create a custom useStateWithCustomComparator hook.
In your the case of your example that would mean to replace
const [myInstance, setMyInstance] = useState(..)
with
const [myInstance, setMyInstance] = useStateWithCustomComparator(..)
The code for my custom hook in Typescript looks like that:
const useStateWithCustomComparator = <T>(initialState: T, customEqualsComparator: (obj1: T, obj2: T) => boolean) => {
const [state, setState] = useState(initialState);
const changeStateIfNotEqual = (newState: any) => {
if (!customEqualsComparator(state, newState)) {
setState(newState);
}
};
return [state, changeStateIfNotEqual] as const;
};

Resources