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