Related
When I load my Nextjs page, I get this error message: "Error: Rendered more hooks than during the previous render."
If I add that if (!router.isReady) return null after the useEffect code, the page does not have access to the solutionId on the initial load, causing an error for the useDocument hook, which requires the solutionId to fetch the document from the database.
Therefore, this thread does not address my issue.
Anyone, please help me with this issue!
My code:
const SolutionEditForm = () => {
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
if (!router.isReady) return null
const { document } = useDocument("solutions", solutionId)
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
return (
<div>
// JSX code
</div>
)
}
useDocument hook:
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id])
return { document, isLoading, error }
}
You cannot call a hook, useEffect, your custom useDocument, or any other after a condition. The condition in your case is this early return if (!router.isReady) returns null. As you can read on Rules of Hooks:
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...
Just remove that if (!router.isReady) returns null from SolutionEditForm and change useDocument as below.
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return; // if there is no id, do nothing ๐๐ฝ
const ref = doc(db, c, id);
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false);
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id });
setError(null);
} else {
setError("No such document exists");
}
},
(err) => {
console.log(err.message);
setIsLoading(false);
setError("failed to get document");
}
);
return () => unsubscribe();
}, [c, id]);
return { document, isLoading, error };
};
The if (!router.isReady) return null statement caused the function to end early, and subsequent hooks are not executed.
You need to restructure your hooks such that none of them are conditional:
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
const { document } = useDocument("solutions", solutionId, router.isReady) // pass a flag to disable until ready
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
// Move this to after the hooks.
if (!router.isReady) return null
and then to make useDocument avoid sending extra calls:
export const useDocument = (c, id, enabled) => {
and updated the effect with a check:
useEffect(() => {
if (!enabled) return;
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id, enabled])
UseEffect cannot be called conditionally
UseEffect is called only on the client side.
If you make minimal representation, possible to try fix this error
I'm trying to access the latest state in other setState function, cant figure out the correct way of doing it for a functional component
without accessing the latest state setMoviesListset state as undefined and causes issues
state
const [movies, setMoviesList] = useState();
const [currentGenre, setcurrentGenre] = useState();
const [page, setPage] = useState(1);
const [genreList, setGenreList] = useState();
const [nextPage, setNextPage] = useState(false);
const [previousMovieList, setPreviousMovieList] = useState();
useEffect(() => {
async function getMovies(currentGenre, page) {
if (currentGenre) {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_genres=${currentGenre}`
);
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data.data];
}
}
});
setMoviesList(previousMovieList.results);
} else {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}`
);
if (!previousMovieList) {
console.log('!previousMovieList', previousMovieList);
console.log('!data', data.data);
setPreviousMovieList(previousMovieList)
} else {
if (nextPage) {
console.log('else', previousMovieList);
setNextPage(false);
setPreviousMovieList([...previousMovieList, data.data])
// return [...previousMovieList, data.data];
}
}
setMoviesList(previousMovieList.results);
}
}
getMovies(currentGenre, page);
}, [currentGenre, page, setMoviesList, nextPage]);
want to access latest previousMovieList here
setMoviesList(previousMovieList.results);
You need to include previousMovieList in your useEffect dependency array as follows:
useEffect(()=>
{...},
[currentGenre, page, setMoviesList, nextPage, previousMovieList]
);
Without including it, you will have a stale closure and latest value will not be reflected in your function. This is causing the initial previousMovieList value of undefined to never update within your useEffect logic.
If you dont want it in your useEffect deps, you can use a ref:
const previousMovieList = useRef();
//then in your useEffect
setMoviesList(previousMovieList.current.results)
//and to set it
previousMovieList.current = ... // whatever you want to store
Or you can do something like this:
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data.data];
}
}
setMoviesList(previousMovieList.results);
});
Basically move setMoviesList to within the setPreviousMovieList function where you do have access to previousMovieList.
Kind of hard to tell what you're trying to do, but generally when you want to store the previous value of state, you would use the ref approach. Like usePrevious for example
Add previousMovieList in useEffect dependency array, which allows react to know that its a dependency and reload when the dependency changes
you can use optional chaining to access the data coming from the api
enter code here
const [movies, setMoviesList] = useState();
const [currentGenre, setcurrentGenre] = useState();
const [page, setPage] = useState(1);
const [genreList, setGenreList] = useState();
const [nextPage, setNextPage] = useState(false);
const [previousMovieList, setPreviousMovieList] = useState();
useEffect(() => {
async function getMovies(currentGenre, page) {
if (currentGenre) {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_genres=${currentGenre}`
);
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data?.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data?.data];
}
}
});
setMoviesList(previousMovieList?.results);
} else {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}`
);
if (!previousMovieList) {
console.log('!previousMovieList', previousMovieList);
console.log('!data', data?.data);
setPreviousMovieList(previousMovieList)
} else {
if (nextPage) {
console.log('else', previousMovieList);
setNextPage(false);
setPreviousMovieList([...previousMovieList, data?.data])
// return [...previousMovieList, data?.data];
}
}
setMoviesList(previousMovieList.results);
}
}
getMovies(currentGenre, page);
}, [currentGenre, page, setMoviesList, nextPage]);
i've been solving this problem without any progress for the pas 2 hours or so, here is code:
export const useFetchAll = () => {
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value);
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
console.log(searchItem);
};
useEffect(() => {
const searchRepo = async () => {
setLoading(true);
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
if (searchItem) searchRepo();
}, [searchItem]);
the problem is that when i enter characters in input and set state to event.target.value it doesn't pick up last character. here is an image:
enter image description here
BTW this is a custom hook, i return the onchange function here:
const HomePage = () => {
const { searchResult, loading, searchItem, handleChange, listToDisplay } =
useFetchAll();
and then pass it as a prop to a component like so:
<Stack spacing={2}>
<Search searchItem={searchItem} handleChange={handleChange} />
</Stack>
</Container>
any help? thanks in advance.
You are handling the searchItem and searchResult state variables as if their state change was synchronous (via setSearchItem and setSearchResult) but it isn't! React state setters are asynchronous.
The useEffect callback has a dependency on the searchItem state variable. Now every time the user types something, the state will change, that change will trigger a re-rendering of the Component and after that render finishes, the side-effect (the useEffect callback) will be executed due to the Components' lifecycle.
In our case, we don't want to initiate the fetch request on the next render, but right at the moment that the user enters something on the search input field, that is when the handleChange gets triggered.
In order to make the code work as expected, we need some a more structural refactoring.
You can get rid of the useEffect and handle the flow through the handleChange method:
export const useFetchAll = () => {
const [ loading, setLoading ] = useState( false );
const [ searchItem, setSearchItem ] = useState( "" );
const [ listToDisplay, setListToDisplay ] = useState( [] );
const handleChange = async ( e ) => {
const { value } = e.target;
// Return early if the input is an empty string:
setSearchItem( value );
if ( value === "" ) {
return setListToDisplay( [] );
}
setLoading( true );
const { data } = await axios.get( "https://api.github.com/repositories" );
setLoading( false );
const valueLowercase = value.toLowerCase(); // Tiny optimization so that we don't run the toLowerCase operation on each iteration of the filter process below
setListToDisplay(
data.filter(({ name }) => name.toLowerCase().includes(valueLowercase))
);
};
return {
searchItem,
handleChange,
loading,
listToDisplay,
};
};
function used for updating state value is asynchronous that why your state variable is showing previous value and not the updated value.
I have made some change you can try running the below code .
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value); // this sets value asyncronously
console.log("e.target.value :" + e.target.value); // event.target.value does not omitting last character
console.log("searchItem :" + searchItem); // if we check the value then it is not set. it will update asyncronously
};
const setList = async () => {
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
};
const searchRepo = async () => {
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
// this useeffect execute its call back when searchItem changes a
useEffect(() => {
setList(); // called here to use previous value stored in 'searchResult' and display something ( uncomment it if you want to display only updated value )
if (searchItem) searchRepo();
}, [searchItem]);
// this useeffect execute when axios set fetched data in 'searchResult'
useEffect(() => {
setList();
}, [searchResult]);
// this useeffect execute when data is updated in 'listToDisplay'
useEffect(() => {
console.log("filtered Data") // final 'listToDisplay' will be availble here
console.log(listToDisplay)
}, [listToDisplay]);
As the title said, what is the correct way of using custom hook to handle onClick Event?
This codesandbox application will display a new quote on the screen when user clicks the search button.
function App() {
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://api.quotable.io/random"
);
return (
<Fragment>
<button disabled={isLoading} onClick={doFetch}>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? <div>Loading ...</div> : <div>{data.content}</div>}
</Fragment>
);
}
I created a custom hook called useDataApi() which would fetch a new quote from an API. In order to update the quote when the user clicks the button, inside the useDataApi(), I created a handleClick() which will change the value of a click value to trigger re-render. And this handleClick() function will be return back to App()
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [click, setClick] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const handleClick = () => {
setClick(!click);
};
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [initialUrl, click]);
return [{ data, isLoading, isError }, handleClick];
};
This is working, however, I don't feel this is the correct solution.
I also tried moving the fetchData() out of useEffect and return the fetchData(), and it works too. But according to the React Doc, it says it is recommended to move functions inside the useEffect.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, []);
return [{ data, isLoading, isError }, fetchData];
};
In addition, for creating these kinds of application, is the way that I am using is fine or there is another correct solution such as not using any useEffects or not create any custom Hook?
Thanks
Not sure if this is correct, but here is my solution.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const doFetch = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
return [{ data, isLoading, isError }, doFetch];
};
Btw, don't mutate state directly.
const handleClick = () => {
setClick(!click); // don't do this
setClick(prev => !prev); // use this
};
Your implementation is fine. We are also using something similar. Hope you find it useful.
function useApi(promiseFunction, deps, shouldRun=true){
// promisFunction returns promise
const [loading, setLoading] = useState(false)
const [data, setData] = useState(false)
const [error, setError] = useState(false)
const dependencies: any[] = useMemo(()=>{
return [...dependencyArray, shouldRun]
},[...dependencyArray, shouldRun])
const reload = () => {
async function call() {
try {
setError(null)
setLoading(true)
const res = await promiseFunction();
}
catch (error) {
setError(error)
}
finally {
setLoading(false)
}
}
call();
}
useEffect(() => {
if(!shouldRun) return
setResult(null) //no stale data
reload()
}, dependencies)
return {loading, error, data, reload, setState: setData}
}
Below code will provide some idea about how to use it.
function getUsersList(){
return fetch('/users')
}
function getUserDetail(id){
return fetch(`/user/${id}`)
}
const {loading, error, data } = useApi(getUsersList, [], true)
const {loading: userLoading, error: userError, data: userData}
= useApi(()=>getUserDetail(id), [id], true)
I have something like:
const [loading, setLoading] = useState(false);
...
setLoading(true);
doSomething(); // <--- when here, loading is still false.
Setting state is still async, so what's the best way to wait for this setLoading() call to be finished?
The setLoading() doesn't seem to accept a callback like setState() used to.
an example
class-based
getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (this.state.pagesSeen.includes(this.state.page + 1)) {
return this.setState({
page: this.state.page + 1,
});
}
if (this.state.prefetchedOrders) {
const allOrders = this.state.orders.concat(this.state.prefetchedOrders);
return this.setState({
orders: allOrders,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
prefetchedOrders: null,
});
}
this.setState(
{
isLoading: true,
},
() => {
getOrders({
page: this.state.page + 1,
query: this.state.query,
held: this.state.holdMode,
statuses: filterMap[this.state.filterBy],
})
.then((o) => {
const { orders } = o.data;
const allOrders = this.state.orders.concat(orders);
this.setState({
orders: allOrders,
isLoading: false,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
// Just in case we're in the middle of a prefetch.
prefetchedOrders: null,
});
})
.catch(e => console.error(e.message));
},
);
};
convert to function-based
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
};
In the above, we want to run each setWhatever call sequentially. Does this mean we need to set up many different useEffect hooks to replicate this behavior?
useState setter doesn't provide a callback after state update is done like setState does in React class components. In order to replicate the same behaviour, you can make use of the a similar pattern like componentDidUpdate lifecycle method in React class components with useEffect using Hooks
useEffect hooks takes the second parameter as an array of values which React needs to monitor for change after the render cycle is complete.
const [loading, setLoading] = useState(false);
...
useEffect(() => {
doSomething(); // This is be executed when `loading` state changes
}, [loading])
setLoading(true);
EDIT
Unlike setState, the updater for useState hook doesn't have a callback, but you can always use a useEffect to replicate the above behaviour. However you need to determine the loading change
The functional approach to your code would look like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const prevLoading = usePrevious(isLoading);
useEffect(() => {
if (!prevLoading && isLoading) {
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
}
}, [isLoading, preFetchedOrders, orders, page, pagesSeen]);
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
};
Wait until your component re-render.
const [loading, setLoading] = useState(false);
useEffect(() => {
if (loading) {
doSomething();
}
}, [loading]);
setLoading(true);
You can improve clarity with something like:
function doSomething() {
// your side effects
// return () => { }
}
function useEffectIf(condition, fn) {
useEffect(() => condition && fn(), [condition])
}
function App() {
const [loading, setLoading] = useState(false);
useEffectIf(loading, doSomething)
return (
<>
<div>{loading}</div>
<button onClick={() => setLoading(true)}>Click Me</button>
</>
);
}
Created a custom useState hook which works similar to the normal useState hook except that the state updater function for this custom hook takes a callback that will be executed after the state is updated and component rerendered.
Typescript Solution
import { useEffect, useRef, useState } from 'react';
type OnUpdateCallback<T> = (s: T) => void;
type SetStateUpdaterCallback<T> = (s: T) => T;
type SetStateAction<T> = (newState: T | SetStateUpdaterCallback<T>, callback?: OnUpdateCallback<T>) => void;
export function useCustomState<T>(init: T): [T, SetStateAction<T>];
export function useCustomState<T = undefined>(init?: T): [T | undefined, SetStateAction<T | undefined>];
export function useCustomState<T>(init: T): [T, SetStateAction<T>] {
const [state, setState] = useState<T>(init);
const cbRef = useRef<OnUpdateCallback<T>>();
const setCustomState: SetStateAction<T> = (newState, callback?): void => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Javascript solution
import { useEffect, useRef, useState } from 'react';
export function useCustomState(init) {
const [state, setState] = useState(init);
const cbRef = useRef();
const setCustomState = (newState, callback) => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Usage
const [state, setState] = useCustomState(myInitialValue);
...
setState(myNewValueOrStateUpdaterCallback, () => {
// Function called after state update and component rerender
})
you can create a async state hooks
const useAsyncState = initialState => {
const [state, setState] = useState(initialState);
const asyncSetState = value => {
return new Promise(resolve => {
setState(value);
setState((current) => {
resolve(current);
return current;
});
});
};
return [state, asyncSetState];
};
then
const [loading, setLoading] = useAsyncState(false)
const submit = async () => {
await setLoading(true)
dosomething()
}
I have a suggestion for this.
You could possibly use a React Ref to store the state of the state variable. Then update the state variable with the react ref. This will render a page refresh, and then use the React Ref in the async function.
const stateRef = React.useRef().current
const [state,setState] = useState(stateRef);
async function some() {
stateRef = { some: 'value' }
setState(stateRef) // Triggers re-render
await some2();
}
async function some2() {
await someHTTPFunctionCall(stateRef.some)
stateRef = null;
setState(stateRef) // Triggers re-render
}
Pass a function to the setter instead of value!
instead of giving a new value to the setter directly, pass it an arrow function that takes the current state value and returns the new value.
it will force it to chain the state updates and after it's done with all of them, it will rerender the component.
const [counter, setCounter] = useState(0);
const incrementCount = () => {
setCounter( (counter) => { return counter + 1 } )
}
now every time incrementCount is called, it will increase the count by one and it will no longer be stuck at 1.