Should localstorage be set from within react callbacks? - reactjs

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.

Related

Why or when should I use state within a custom Hook in React?

I am learning about custom Hooks in React. I have taken the following typical example (useFetch) as you can see in https://www.w3schools.com/react/react_customhooks.asp.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null); /* <--- Why use this? */
useEffect(() => {
fetch(url).then((res) => res.json()).then((data) => setData(data));
}, [url]);
return [data];
};
export default useFetch;
I'm confused why state should be used inside that Hook, or in general in custom Hooks. I always relate state management to components, not to a hook. Hence perhaps my confusion.
Couldn't it have been done simply by returning a data variable?
Unlike normal functions, custom hooks encapsulates React state. This means that the hook can utilize react's own hooks and perform custom logic. It's quite abstract in that sense.
For your case, you want to return the state of the data, not just the data by itself because the state is tied to a useEffect. That means that fetch will only run and by extension only update data when its dependencies ([url]) are changed.
If it was a normal function just returning the data from the fetch, you would send a request every time the component using the hook re-renders. That's why you use useState coupled with useEffect to make sure it only updates when it should.

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])

Handling API calls using useEffect vs using useCallback

This is very likely a dumb question -- I have the understanding that anything that triggers a side-effect should be handled with useEffect. I was wondering if that understanding is correct. Particularly in the context of making an API call -- is it good to make API calls in a useCallback hook?
If you want to do it based on some kind of prop or state change, use the useEffect hook, e.g.,
useEffect(async ()=> {
const user = await userApi.get(props.id) // Api call here
setUser(user)
}, [props.id]})
If you want to do so on a button click (or any event),
const handleClick = () => {
const user = await userApi.get(props.id)
setUser(user)
}
useCallback isn't really relied on for api calls or side-effects. useCallback is basically storing a "version" of a function, based on dependencies. When a dependency changes, you get a new function, because what it returns will be different, e.g.,
// props.id = 1
const doSomethingCallback = useCallback(() => {
/* do something with props.id */
}, [props.id])
While props.id === 1, doSomethingCallback will always reference the function as it was declared on the first render. If props.id changes to 2, useCallback will reference a new function. So, if doSomethingCallback was a dependency of a useEffect, when props.id changed to 2, useCallback would point to a new function, which would then get noticed by the useEffect and run whatever was in it, e.g.,
useEffect(() => { /* side-effect stuff */}, [doSomethingCallback])

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