Use set function from custom hooks inside useEffect - reactjs

I created a custom hook that maintain state in a certain type based on data in a different type like this:
import { useState, useRef, Dispatch } from 'react';
function useBackedState<TData, TBacking>(initialData: TBacking, converter: (backing: TBacking) => TData):
[
state: TData,
getBackingData: () => TBacking,
setData: Dispatch<TBacking>
] {
const ref = useRef(initialData);
const [state, setState] = useState(converter(initialData));
const setData: Dispatch<TBacking> = (val: TBacking) => {
ref.current = val;
setState(converter(val));
};
return [
state,
() => ref.current,
setData
];
}
export default useBackedState;
However, I have problems when I call on setData inside a useEffect hook.
For example, I tested it in this component:
import { useState } from 'react';
import useBackedState from './hooks/useBackedState';
function Component() {
const [data, backing, setData] = useBackedState<string, string[]>(
[],
(arr) => arr.join('');
);
const [somethingElse, setSomethingElse] = useState('a');
useEffect(() => {
setSomethingElse('b');
setData(['h', 'e', 'l', 'l', 'o']);
}, []);
return (
<div>
<p>{data}</p>
... some more rendering
</div>
);
}
I want useEffect to only run once at the start, so I provided an empty array of dependencies. However, although the app works, I get this warning:
React Hook useEffect has a missing dependency: 'setData'. Either
include it or remove the dependency array
If I do add setData as a dependency, the entire app goes into an infinite loop of refresh and I get this error:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I am not required to add the setSomethingElse as a dependency to useEffect. Why am I required to add setData?

All state and prop values that get referenced inside a hook should have that value in the dependency array - so the argument goes. If you agree and wish to satisfy the linter, here, it's easy - just memoize the functions returned from the custom hook (which is often a good practice anyway, especially when exhaustive-deps is being used).
const setData: Dispatch<TBacking> = useCallback((val: TBacking) => {
ref.current = val;
setState(converter(val));
}, [setState, converter]);
I am not required to add the setSomethingElse as a dependency to useEffect
The linter can see that setSomethingElse comes directly from a state setter function declared in the same component, and thus definitely won't ever change, and so doesn't need to be in the dependency array.

Related

Should localstorage be set from within react callbacks?

In a (typescript) react app, I have some hooks for reading and writing to local storage, like this:
import { useEffect, useState } from "react";
...
export const useLocalStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue);
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Elsewhere in the app, I have a UX element that needs to store some data in local storage, as part of an onClick() callback:
myValue, setMyValue = useLocalStorage("MY_KEY", 0);
...
onClick() => {
setMyValue("some data");
}
However, this means calling useEffect() from within a callback, which violates the hook rules.
Is it conventional here to just call localstorage.setItem() directly from withing the callback, or is there a more idiomatic way to refactor this code?
I think you're a little confused about what a hook is. Consider this snipet.
function Button() {
const [wasClicked, setWasClicked] = useState(false);
function handleClick() {
setWasClicked(true) // completely legal.
// this is not "calling a hook"
const [clickedTime, setClickedTime] = useState(Date.now()) // illegal
// this is "calling a hook"
}
return <button disabled={wasClicked} onclick={handleClick}>click!</button>
}
Calling a hook like useState(false) is as you say not permitted within callbacks. This is because the order in which hooks are called is actually super important to React. So, you can't conditionally call hooks, and you cant call them from a callback, you have to call them at the top level of your component.
That being said, setWasClicked is not a hook, it's just a regular function that happens to be returned from a hook. You can call this function from anywhere, because as stated it is not a hook.
In your case, useLocalStorage is a hook, you have to follow the rules of hooks. However, it returns setValue which is not a hook, just a regular function returned by the useState call. That triggers the useEffect callback to run, but it doesn't re-run useEffect. useEffect was called only when you called useLocalStorage.
TLDR:
To answer your question, I would put local storage stuff in a hook. You do want to use the useEffect hook because you don't want to access localStorage on every render, only when dependencies change.

React Hook "useEffect" cannot be called inside a callback error occurs

The error below sometimes occurs I don't know why. Every code seems to work but only an error occurs.
React Hook "useEffect" 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
Search for the keywords to learn more about each error.
import { useEffect, useState } from 'react'
import { useAuthContext } from 'src/contexts/Auth'
import { brandRepository } from 'src/repositories/BrandRepository'
export default function TestBrand() {
const [brands, setBrands] = useState<any[]>()
const { currentUser } = useAuthContext()
useEffect(() => {
if(!currentUser) return
useEffect(() => {
brandRepository.list(currentUser.id).then(async (docs: any[]) => {
if(!docs) return
await setBrands(docs)
})
}, [])
}, [currentUser])
if(!currentUser) {
return <div>Loading...</div>
}
return (
<div>test</div>
)
}
You cant call useEffect or other hooks inside a function or CB you can only call hooks inside Function component
https://reactjs.org/docs/hooks-rules.html
Blockquote
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls
You cannot use useEffect inside another useEffect. Try using both the hooks separately as it forbids the rules of hooks. Better don't use the second useEffect as I do not see any specific use of it. Refer this https://reactjs.org/docs/hooks-rules.html
It seems that in useEffect you are making a network request for one time if the currentUser exists. In this case you don't need 2 useEffect. if can do it like this
useEffect(() => {
if(!currentUser) return
brandRepository.list(currentUser.id).then(async (docs: any[]) => {
if(!docs) return
await setBrands(docs)
})
}, [currentUser])

Eslint error on using callback hooks in React

I am using callback hooks to stop the rendering of components that change due to a change of function passed as props, in every render. Here, I have added callback Hooks to incrementSalary and incrementAge. Callback hooks in this case seem to work fine. However eslint throws the following error:
Line 10:76: React Hook useCallback has an unnecessary dependency: 'age'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
Line 15:5: React Hook useCallback has an unnecessary dependency: 'salary'. Either exclude it or remove the dependency array react-hooks/exhaustive-dep
If I do not include callback hook, and I click on any of the button. It renders all other buttons in the parent component. I believe this happens because my functions are not memoized and using callback hooks and adding dependencies makes my functions memoized. So only the specific button which s clicked is rendered. I do not understand why eslint is throwing errors at me, is it due to the wrong use of callback or due to the wrong way of using prevState?
import React, { useState, useCallback } from "react";
import Title from "./Title";
import Count from "./Count";
import Button from "./Button";
function ParentComponent() {
const [age, setAge] = useState(22);
const [salary, setSalary] = useState(25000);
const incrementAge = useCallback(() => setAge((prevAge) => prevAge + 1), [
age,
]);
const incrementSalary = useCallback(
() => setSalary((prevSalary) => prevSalary + 5000),
[salary]
);
return (
<div>
<Title>Use Callback Hook</Title>
<Count text="Age" count={age} />
<Button handleClick={incrementAge}>Increment Age</Button>
<Count text="Salary" count={salary} />
<Button handleClick={incrementSalary}>Increment Salary</Button>
</div>
);
}
export default ParentComponent;
This is a warning/error that is coming for useCallback hook.
You don't need to add age and salary as dependencies in the useCallback hook. This is because there is no actual dependency on these states inside the callback.
const incrementAge = useCallback(
() => setAge((prevAge) => prevAge + 1),
[]); // Remove dependency.
const incrementSalary = useCallback(
() => setSalary((prevSalary) => prevSalary + 5000),
[]); // Remove dependency.

Missing dependency: 'dispatch' in React

In my react/redux app, i'm using dispatch to call the action that retrieve data from state in redux each time the component is mounted. The problem is happening on useState My way does not work
Below is the error I'm getting:
React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array. Outer scope values like 'getInvoiceData' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
Here is my code:
const TableSection = () => {
const invoiceData = useSelector((state => state.tables.invoiceData));
const dispatch = useDispatch()
useEffect(() => {
dispatch(getInvoiceData());
}, [getInvoiceData]);
(...)
export default TableSection;
You need to add dispatch function to dep array:
const TableSection = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(getInvoiceData());
}, [dispatch]);
Its safe to add it to dep array because its identity is stable across renders, see docs.
Note: like in React's useReducer, the returned dispatch function identity is stable and won't change on re-renders (unless you change the store being passed to the , which would be extremely unusual).
This is not an error, its just a warning.
You can fix this by adding dispatch in the dependency array.
useEffect(() => {
dispatch(getInvoiceData());
}, [dispatch]);
second part of the warning message states, Outer scope values like 'getInvoiceData' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps, you also need to remove getInvoiceData function from the dependency array of useEffect hook.
Anything from the scope of the functional component, that participates in react's data flow, that you use inside the callback function of useEffect should be added in the dependency array of the useEffect hook.
Although, in your case, it is safe to omit dispatch function from the dependency array because its guaranteed to never change but still it won't do any harm if you add it as a dependency.

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