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.
Related
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.
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])
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.
I'm trying to learn why it's necessary to use useCallback in this situation:
function MyComponent() {
const navigation = useContext(NavigationContext);
const redirect = useCallback(() => {
navigation.navigate("Home");
});
useEffect(() => {
redirect();
}, [redirect]);
}
I can't use navigation directly inside useEffect() and also I can't refer the function without useCallback(). I don't know why I can use other objects like Firebase Context (from a Firestore instance) without any problem inside useEffect, but I can't use navigation directly.
From a quick look in the equivalent codebases:
Firebase Context provider is returned memoized
whilst the react-navigation one just returns the object.
Why does React Hooks Axios API Call return twice.. even with a check if it's loaded?
I am used to this.setState but trying to understand the reason behind this why it's showing up twice inside my console log.
My code:
import React, { useState, useEffect } from "react";
import axios from "axios";
const App = () => {
const [users, setUsers] = useState({ results: [] });
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
await axios
.get("https://jsonplaceholder.typicode.com/users/")
.then(result => setUsers(result));
setIsLoading(false);
};
fetchData();
}, []);
console.log(
users.status === 200 && users.data.map(name => console.log(name))
);
return <h2>App</h2>;
};
export default App;
For the fire twice, perhaps is one time from componentDidMount, and the other one from componentDidUpdate
https://reactjs.org/docs/hooks-effect.html
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
You did not check 'isLoading' before you fire the API call
// Only load when is not loading
if (!isLoading) {
fetchData();
}
This is actually normal behavior. This is just how functional components work with useEffect(). In a class component the "this" keyword is mutated when the state changes. In functional components with hooks the function is called again, and each function has its own state. Since you updated state twice the function was called twice.
You can read these 2 very in dept articles by one of the creators of react hooks for more details.
https://overreacted.io/a-complete-guide-to-useeffect/
https://overreacted.io/how-are-function-components-different-from-classes/
Even I had a similar issue. in my case useEffect() was called twice because the parent component was re-rendered because of a data change.