I am using useAxios hook to fetch data from Api then use add data to global state by Constate library.
However, I am getting error: "Maximum update depth exceeded ". So, how to make it work ? Also, what is best practice to handle asynchronous Api and global state in my use case ?
In index Component:
const {loading, error} = useGetFavoriteTracks()
const {state} = useTrackContext()
In useGetFavoriteTracks.js, fetch data then add favorite tracks to global state
export const useGetFavoriteTracks = params => {
const { data, error, loading } = useAxios({
axiosInstance: myApiAxiosInstance,
url: myApiEndPoint.FAVORITE_TRACKS
});
const { addFavoriteTracks } = useTrackContext();
addFavoriteTracks(data); //add favorite tracks to global state
return { loading, error };
};
TrackContext.js
const useTrack = params => {
const [state, dispatch] = useReducer(reducer, initialState);
const addFavoriteTracks = (data) => {
dispatch({
type: trackActionTypes.FAVORITE_TRACKS_FETCHED_SUCCESS,
payload: data
})
}
return {state, addFavoriteTracks}
}
export const [TrackProvider, useTrackContext] = constate(useTrack);
Rule of thumb: any direct or indirect call to setState or dispatch must not be fired in the process of rendering.
In useGetFavoriteTracks you must not unconditionally call addFavoriteTracks(data); like that. Put that call inside useEffect.
useEffect(() => {
if (!loading && data) addFavoriteTracks(data);
}, [loading, data])
Related
I'm fairly new to the context API and react hooks beyond useState and useEffect so please bare with me.
I'm trying to create a custom useGet hook that I can use to GET some data from the backend then store this using the context API, so that if I useGet again elsewhere in the app with the same context, it can first check to see if the data has been retrieved and save some time and resources having to do another GET request. I'm trying to write it to be used generally with various different data and context.
I've got most of it working up until I come to try and dispatch the data to useReducer state and then I get the error:
Hooks can only be called inside the body of a function component.
I know I'm probably breaking the rules of hooks with my call to dispatch, but I don't understand why only one of my calls throws the error, or how to fix it to do what I need. Any help would be greatly appreciated.
commandsContext.js
import React, { useReducer, useContext } from "react";
const CommandsState = React.createContext({});
const CommandsDispatch = React.createContext(null);
function CommandsContextProvider({ children }) {
const [state, dispatch] = useReducer({});
return (
<CommandsState.Provider value={state}>
<CommandsDispatch.Provider value={dispatch}>
{children}
</CommandsDispatch.Provider>
</CommandsState.Provider>
);
}
function useCommandsState() {
const context = useContext(CommandsState);
if (context === undefined) {
throw new Error("Must be within CommandsState.Provider");
}
return context;
}
function useCommandsDispatch() {
const context = useContext(CommandsDispatch);
if (context === undefined) {
throw new Error("Must be within CommandsDispatch.Provider");
}
return context;
}
export { CommandsContextProvider, useCommandsState, useCommandsDispatch };
useGet.js
import { API } from "aws-amplify";
import { useRef, useEffect, useReducer } from "react";
export default function useGet(url, useContextState, useContextDispatch) {
const stateRef = useRef(useContextState);
const dispatchRef = useRef(useContextDispatch);
const initialState = {
status: "idle",
error: null,
data: [],
};
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "FETCHING":
return { ...initialState, status: "fetching" };
case "FETCHED":
return { ...initialState, status: "fetched", data: action.payload };
case "ERROR":
return { ...initialState, status: "error", error: action.payload };
default:
return state;
}
}, initialState);
useEffect(() => {
if (!url) return;
const getData = async () => {
dispatch({ type: "FETCHING" });
if (stateRef.current[url]) { // < Why doesn't this also cause an error
const data = stateRef.current[url];
dispatch({ type: "FETCHED", payload: data });
} else {
try {
const response = await API.get("talkbackBE", url);
dispatchRef.current({ url: response }); // < This causes the error
dispatch({ type: "FETCHED", payload: response });
} catch (error) {
dispatch({ type: "ERROR", payload: error.message });
}
}
};
getData();
}, [url]);
return state;
}
EDIT --
useCommandsState and useCommandsDispatch are imported to this component where I call useGet passing the down.
import {
useCommandsState,
useCommandsDispatch,
} from "../../contexts/commandsContext.js";
export default function General({ userId }) {
const commands = useGet(
"/commands?userId=" + userId,
useCommandsState,
useCommandsDispatch
);
Why am I only getting an error for the dispatchRef.current, and not the stateRef.current, When they both do exactly the same thing for the state/dispatch of useReducer?
How can I refactor this to solve my problem? To summarise, I need to be able to call useGet in two or more places for each context with the first time it's called the data being stored in the context passed.
Here are various links to things I have been reading, which have helped me to get this far.
How to combine custom hook for data fetching and context?
Updating useReducer 'state' using useEffect
Accessing context from useEffect
https://reactjs.org/warnings/invalid-hook-call-warning.html
I think your problem is because you are using useRef instead of state for storing state. If you useRef for storing state you need to manually tell react to update.
I personally would not use reducer and just stick to the hooks you are familiar with as they fulfill your current requirements. I also think they are the best tools for this simple task and are easier to follow.
Code
useGetFromApi.js
This is a generalized and reusable hook - can be used inside and outside of the context
export const useGetFromApi = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!url) return;
const getData = async () => {
try {
setLoading(true);
setData(await API.get('talkbackBE', url));
} catch ({ message }) {
setError(message);
} finally {
setLoading(false); // always set loading to false
}
};
getData();
}, [url]);
return { data, error, loading };
};
dataProvider.js
export const DataContext = createContext(null);
export const DataProvider = ({ children, url}) => {
const { data, error, loading } = useGetFromApi(url);
return (
<DataContext.Provider value={{ data, error, loading }}>
{children}
</DataContext.Provider>
);
};
useGet.js
Don't need to check if context is undefined - React will let you know
export const useGet = () => useContext(DataContext);
Usage
Most parent wrapping component that needs access to data. This level doesn't have access to the data - only it's children do!
const PageorLayout = ({children}) => (
<DataProvider url="">{children}</DataProvider>
)
A page or component that is nested inside of the context
const NestedPageorComponent = () => {
const {data, error, loading } = useGet();
if(error) return 'error';
if(loading) return 'loading';
return <></>;
}
Hopefully this is helpful!
Note I wrote most of this on Stack in the editor so I was unable to test the code but it should provide a solid example
In my React functional component, I have the following code;
const user = useFetch('api/userinfo', {});
Essentially, this is a custom hook call and internally it has a fetch call to the API and sets the data (below is relevant code inside usefetch);
const [data, setData] = useState(initialData);
//....fetch call
setData(json); // once data is fetched
In my main component, since my grid depends on this data, how do I make the code wait to proceed to the Grid jsx till data is fetched? I was planning to use async..await. But not sure if it is possible to do that here with custom hooks?
With below code, seems like the hooks is getting invoked multiple times for some reason;
export default function useFetch(initialUrl, initialData) {
const [url] = useState(initialUrl);
const [loadingData, setLoadingData] = useState(true);
const [data, setData] = useState(initialData);
useEffect(() => {
setLoadingData(true);
fetch(url)
.then(response => {
if (response.status === 200) {
response.json().then(json => {
setData(json);
setLoadingData(false);
});
})
}, [url]);
return [loadingData, data];
}
A couple options for you:
Use another state variable (ie some boolean) and use that to keep track of whether or not the data comes back from the API. Then conditionally render some 'loading' element
Check to see if the data exists and conditionally render based on its existence.
Here's how you can do it with your custom hook:
// defining useFetch hook
const useFetch = (url) => {
// state to keep track of loading
const [loadingData, setLoadingData] = useState(false);
// state for data itself
const [data, setData] = useState(null);
// effect to fetch data
useEffect(() => {
const fetchData = async () => {
try {
// set data to loading
setLoadingData(true);
// request to load data, you can use fetch API too
const { data } = await axios.get(url);
// set data in state and loading to false
setLoadingData(false);
setData(data);
} catch (error) {
console.log("error", error);
}
};
fetchData();
}, [url]);
// return the data and loading state from this hook
return [loadingData, data];
};
Now, you can use this hook in your component like:
const MyComponent = (props) => {
const [isDataLoading, data] = useFetch('/api/some-url');
// now check if data is loading, if loading then return a loader/spinner
if (isDataLoading || !data) return <p>Data is loading...</p>
// otherwise render your actual component
return (
<div>
<h1>This is my component with data</h1>
</div>
);
}
I have following structure in a react app:
<SomeProvider id={objectId}>
<SomeApp />
</SomeProvider>
This provider uses a useQuery to fetch an object from the backend. With useContext I make it accessible to all the components of the app:
const EventProvider = ({ id, children }) => {
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
// etc.
}
export const useSomething = () => {
// etc.
}
In a component I can have access to this object:
const Component = ({id}) => {
const { obj } = useSomething()
}
Until here all working. My question is, inside this component, I have a button that changes this object in the backend.
How can I fetch the obj again?
I can of course refresh the page, but this is the solution that I want to avoid. What I've tried so far is:
Try to use the useQuery again in the Component.
const Component = ({id}) => {
const { obj } = useSomething()
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
}
But actually what I would like to do is trigger the query when a State variable changes:
const Component = ({id}) => {
const { obj } = useSomething()
const { isActivated } = useOtherHook()
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
useEffect(() => {
// when isActivated changes, I would like to fetch the obj again
}, [isActivated])
}
If I use useQuery inside of useEffect, I get:
Hooks can only be called inside of the body of a function component
What is the best approach to solve this challenge?
The reason for the error messages you get is, that you can only use hooks in the body of your component, not inside of other functions that are not hooks, nor inside of conditions. You can read more about that here in the rules of hooks
As you can read in the documentation for useQuery here, there are multiple ways of keeping data up to date or refetching.
The easiest way of keeping data up to date would be to use the polling feature from apollo.
const { loading, error, data } = useQuery(QUERY, {
variables: input,
skip: !isActivated,
pollInterval: 500, // Update every 500ms
});
One way of refetching on demand would be to use the returned refetch function.
const { loading, error, data, refetch } = useQuery(/* ... */);
useEffect(() => { refetch() }, [isActivated])
Depending on your needs, you could also use the useLazyQuery hook. This does not fetch data on render but only on function call. This is most useful when you want to only request data when the input has been set or changed.
const [getData, { loading, data }] = useLazyQuery(QUERY);
useEffect(() => { getData({ variables: input }) }, []);
I'm wondering what the recommended best practice is for manipulating and exposing the new React Context.
The easiest way to manipulate context state seems to be to just attach a function to the context that either dispatches (usereducer) or setstate (useState) to change its internal value once called.
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
return (
<Context.Provider
value={{
todos: state.todos,
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
}}
>
{children}
</Context.Provider>
);
};
export const Todos = id => {
const { todos, fetchTodos } = useContext(Context);
useEffect(() => {
if (fetchTodos) fetchTodos(id);
}, [fetchTodos]);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
I was however told exposing and using the react context object directly is probably not a good idea, and was told to wrap it inside a hook instead.
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
return (
<Context.Provider
value={{
dispatch,
state
}}
>
{children}
</Context.Provider>
);
};
const useTodos = () => {
const { state, dispatch } = useContext(Context);
const [actionCreators, setActionCreators] = useState(null);
useEffect(() => {
setActionCreators({
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
});
}, []);
return {
...state,
...actionCreators
};
};
export const Todos = ({ id }) => {
const { todos, fetchTodos } = useTodos();
useEffect(() => {
if (fetchTodos && id) fetchTodos(id);
}, [fetchTodos]);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
I have made running code examples for both variants here: https://codesandbox.io/s/mzxrjz0v78?fontsize=14
So now I'm a little confused as to which of the 2 ways is the right way to do it?
There is absolute no problem with using useContext directly in a component. It however forces the component which has to use the context value to know what context to use.
If you have multiple components in the App where you want to make use of TodoProvider context or you have multiple Contexts within your app , you simplify it a little with a custom hook
Also one more thing that you must consider when using context is that you shouldn't be creating a new object on each render otherwise all components that are using context will re-render even though nothing would have changed. To do that you can make use of useMemo hook
const Context = React.createContext<{ todos: any; fetchTodos: any }>(undefined);
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
const context = useMemo(() => {
return {
todos: state.todos,
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
};
}, [state.todos, getTodos]);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
const getTodos = async id => {
console.log(id);
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/" + id
);
return await response.json();
};
export const useTodos = () => {
const todoContext = useContext(Context);
return todoContext;
};
export const Todos = ({ id }) => {
const { todos, fetchTodos } = useTodos();
useEffect(() => {
if (fetchTodos) fetchTodos(id);
}, [id]);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
Working demo
EDIT:
Since getTodos is just a function that cannot change, does it make
sense to use that as update argument in useMemo?
It makes sense to pass getTodos to dependency array in useMemo if getTodos method is changing and is called within the functional component. Often you would memoize the method using useCallback so that its not created on every render but only if any of its dependency from enclosing scope changes to update the dependency within its lexical scope. Now in such a case you would need to pass it as a parameter to the dependency array.
However in your case, you can omit it.
Also how would you handle an initial effect. Say if you were to call
`getTodos´ in useEffect hook when provider mounts? Could you memorize
that call as well?
You would simply have an effect within Provider that is called on initial mount
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
const context = useMemo(() => {
return {
todos: state.todos,
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
};
}, [state.todos]);
useEffect(() => {
getTodos();
}, [])
return <Context.Provider value={context}>{children}</Context.Provider>;
};
I don't think there's an official answer, so let's try to use some common sense here. I find perfectly fine to use useContext directly, I don't know who told you not to, perhaps HE/SHE should have pointed for official docs. Why would the React team create that hook if it wasn't supposed to be used? :)
I can understand, however, trying to avoid creating a huge object as the value in the Context.Provider, one that mixes state with functions that manipulate it, possibly with async effects like your example.
However, in your refactor, you introduced a very weird and absolutely unnecessary useState for the action creator that you simply had defined inline in your first approach. It seems to me you were looking for useCallback instead. So, why don't you mix both like this?
const useTodos = () => {
const { state, dispatch } = useContext(Context);
const fetchTodos = useCallback(async id => {
const todos = await getTodos(id)
dispatch({ type: 'SAVE_TODOS', payload: todos })
}, [dispatch])
return {
...state,
fetchTodos
};
}
Your calling code doesn't need that weird check to verify that fetchTodos indeed exists.
export const Todos = id => {
const { todos, fetchTodos } = useContext(Context);
useEffect(() => {
fetchTodos()
}, []);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
Finally, unless you actually need to use this todos + fetchTodos combo from more components down the tree from Todos, which you didn't explictly stated in your question, I think using Context is complicating matters when they're not needed. Remove the extra layer of indirection and call useReducer directly in your useTodos.
It may not be the case here, but I find people are mixing a lot of things in their head and turning something simple into something complicated (like Redux = Context + useReducer).
Hope it helps!
In case we want to fetch data from remote server and then pass it as initial state to a reducer, how we can proceed ? I tried to call dispatch inside useEffect but it was rejected as per rule of calling hooks inside useEffect(...) is forbidden,
Any help ?
The only way you can pass the data as the initial state to a reducer is to fetch the data before rendering your component. You can do that in the parent:
const Parent = () => {
const [data, setData] = useState(null)
useEffect(() => {
apiCall().then(response => {
setData(response.data)
})
}, [])
if (!data) return null
return <MyComponent data={data} />
}
const MyComponent = ({ data }) => {
const [state, dispatch] = useReducer(reducer, data)
... // utilize state
}
.
The above approach is undesirable as it breaks separation of concerns. Usually it is better to set a null initial state and call dispatch to change it to the desired state:
const MyComponent () => {
const [state, dispatch] = useReducer(reducer, null)
useEffect(() => {
apiCall().then(response => {
dispatch({
type: 'INITIALIZE',
data: response.data
})
})
}, [])
if (!state) return null
... // utilize state
}
.