I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.
Related
Error Message: React Hook "useDispatch" is called conditionally. React Hooks must be called in the exact same order in every component render
I've been trying to figure out how to fix this for days, but nothing seeems to work. The component works when I don't mock anything, but as soon as I mock dispatch it gives me this error.
Here's my component:
import { Stage } from "../Stage/Stage";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { retrieveStageList } from "../../modules/reducer";
import { Process } from "../Process/Process";
export function RenderProcess({
_useSelector = useSelector,
_useDispatch = useDispatch(), //this is where it breaks
_Process = Process,
}) {
const dispatch = _useDispatch();
const process = _useSelector((state) => state.renderProcess);
const stageList = _useSelector((state) => state.stageList);
useEffect(() => {
if (process.processId !== null)
dispatch(retrieveStageList(process.processId));
}, []);
return (
<>
<_Process process={process} />
{stageList?.map((stageInputs, processId) => {
return (
<div key={processId}>
<Stage stage={stageInputs} />
</div>
);
})}
</>
);
}
Here's my test for this component:
import { render } from "#testing-library/react";
import { RenderProcess } from "./RenderProcess";
test("should call dispatch once.", () => {
const _useSelector = (fn) =>
fn({
stageList: [],
renderProcess: { processId: "309624b6-9c96-4ba7-8f7e-78831614f685" },
});
const dispatch = jest.fn();
render(
<RenderProcess
_useSelector={_useSelector}
_useDispatch={() => dispatch}
_Process={() => {}}
/>
);
expect(dispatch).toHaveBeenCalledTimes(1);
});
Any help on this would be amazing.
Found the fix, in the properties being passed in RenderProcess- on this
line:
_useDispatch = useDispatch()
it should be:
_useDispatch = useDispatch
I'm trying to combine several contexts that are feed with some async operations, in my app's pages.
I would like to combine these contexts without using the Context.Provider because it could be verbose. For example,
<Route path="/discover">
<MainContainer extraClass="discover-container" hasHeader={true}>
<UserContext>
<ContentContextProvider>
<NotificationContext>
<Discover />
</NotificationContext>
</ContentContextProvider>
</UserContext>
</MainContainer>
</Route>
In each of these Context I wrapper the child with the context. Fe,
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const ContentContext = React.createContext({});
export const ContentContextProvider = ({ children }) => {
const { allContent, setAllContents } = useAllContent([]);
return (
<ContentContext.Provider value={{ allContent, setAllContents }}>
{children}
</ContentContext.Provider>
);
};
export default ContentContext;
This works, but as I mentioned before is very verbose so i would like to use the Contexts like an objetcs to combine between them.
I tried:
import { useState, useEffect, useCallback } from "react";
import { DataStore, Predicates } from "#aws-amplify/datastore";
import { Content } from "models";
const useAllContent = (initialValue) => {
const [allContent, setContent] = useState(initialValue);
const setAllContents = useCallback(async () => {
const contents = await DataStore.query(Content, Predicates.ALL);
setContent(contents);
}, []);
useEffect(() => {
if (allContent === 0) setAllContents();
}, [allContent, setAllContents]);
return { allContent, setAllContents };
};
export default useAllContent;
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const { allContent, setAllContents } = useAllContent([]);
const ContentContext = React.createContext({ allContent, setAllContents });
export default ContentContext;
But I break the rule × Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
How could i achieve it?
Combining instances of React.Context in the way you describe would require manipulating the values they contain. But there is no way to access the value of a Context without using its corresponding Provider. I sympathize with the dislike of verboseness, but it is unavoidable in this case.
Here is how I have created a Context for simple program I am writing
import React, { useState, createContext, useEffect } from "react";
export const PhotoContext = createContext();
export const PhotoProvider = (props) => {
const [photo, setPhoto] = useState([]);
useEffect(() => {
console.log("Use Effect Runs HAHAHAH");
console.log("HAHAHAHAHAHAHAH");
fetchPhotos();
async function fetchPhotos() {
const url =
"https://raw.githubusercontent.com/bobziroll/scrimba-react-bootcamp-images/master/images.json";
fetch(url)
.then((res) => res.json())
.then((arr) => {
setPhoto(arr);
})
.catch(console.log("ERROR"));
}
}, []);
return (
<PhotoContext.Provider value={[photo, setPhoto]}>
{props.children}
</PhotoContext.Provider>
);
};
There is another file where I want to load the data in the photos variable. Here is the code for it. I have used setTimeout to see where exactly is the problem. It seems whenever the statement in setTimeout runs, the value in console in returned twice. First, it is empty and the second has the actual value. But since, I try to access the photos.url, and since the first time it is undefined, the program collapses.
import React, { useState, useContext } from "react";
import { PhotoContext } from "../Context/PhotoContext";
const Photos = (props) => {
const [photos, values] = useContext(PhotoContext);
setTimeout(() => {
console.log(photos[0].url);
}, 3000);
return <div>{}</div>;
};
export default Photos;
Help would be really appreciated.
Didn't see the problem. I created sandbox for your example.
https://codesandbox.io/s/inspiring-lovelace-5r1gb?file=/src/App.js
reducer.js
export const reducer = (state, action) => {
switch(action.type) {
case SET_HEROES:
return { ...state, heroes: action.heroes }
}
}
AppContext.js
export const AppContext = React.createContext()
export const AppProvider = (props) => {
const initialState = {
heroes: []
}
const [appState, dispatch] = useReducer(reducer, initialState)
const setHeroes = async () => {
const result = await getHeroes()
dispatch({ type: SET_HEROES, heroes: result })
}
return <AppContext.Provider
values={{ heroes: appState.heroes, setHeroes }}
>
{props.children}
</AppContext.Provider>
}
HeroesScreen.js
const HeroesScreen = () => {
const { heroes, setHeroes } = useContext(AppContext)
useEffect(() => {
setHeroes()
}, [])
return <>
// iterate hero list
</>
}
export default HeroesScreen
Above is plain simple setup of a component using reducer + context as state management. Heroes are showing on the screen, everything works fine but I'm having a warning Reach Hook useEffect has a missing dependency: 'setHeroes'. But if I add it in as a dependency, it'll crash my app with Maximum depth update exceeded
Been searching but all I see is putting the function fetch call inside the useEffect(). What I would like is to extract the function and put it in a separate file following the SRP principle
EDITED:
As advised on using useCallback()
AppContext.js
const setHeroes = useCallback(() => {
getHeroes().then(result => dispatch({ type: SET_HEROES, heroes: result }))
}, [dispatch, getHeroes])
HeroesScreen.js
useEffect(() => {
setHeroes()
}, [setHeroes])
Adding the getHeroes as dependency on useCallback, linter shows unnecessary dependency
If you look at the code in AppProvider, you are creating a new setHeroes function every time it renders. So if you add setHeroes as a dependency to useEffect the code executes something like this:
AppProvider renders, setHeroes is created, the state is initial state
Somewhere down the component hierarchy HeroesScreen renders. useEffect is called which in turn calls setHeroes
getHeroes is called and an action is dispatched
reducer changes the state which causes AppProvider to re-render
AppProvider renders, setHeroes is created from scratch
The useEffect executes again since setHeroes changed and the whole loop repeats forever!
To fix the issue you indeed need to add setHeroes as a dependency to useEffect but then wrap it using useCallback:
const setHeroes = useCallback(async () => {
const result = await getHeroes();
dispatch({ type: SET_HEROES, heroes: result });
}, [getHeroes, dispatch]);
Good question, I also had this problem, this is my solution. I'm using Typescript but it'll work with JS only as well.
UserProvider.tsx
import * as React from 'react'
import { useReducer, useEffect } from 'react'
import UserContext from './UserContext'
import { getUser } from './actions/profile'
import userReducer, { SET_USER } from './UserReducer'
export default ({ children }: { children?: React.ReactNode }) => {
const [state, dispatch] = useReducer(userReducer, {})
const getUserData = async () => {
const { user } = await getUser()
dispatch({ type: SET_USER, payload: { ...user } })
}
useEffect(() => {
getUserData()
}, [])
return (
<UserContext.Provider value={{ user: state }}>
{children}
</UserContext.Provider>
)
}
Then wrap your App with the provider
index.tsx
<UserProvider>
<App />
</UserProvider>
Then to use the Context Consumer I do this
AnyComponent.tsx
<UserConsumer>
{({ user }) => {
...
}}
</UserConsumer
or you can also use it like this
const { user } = useContext(UserContext)
Let me know if it works.
I used a structure using React Hooks. It is based on a global Context that contains a combination of reducers (as in the Redux).
Also, I widely use custom hooks to separate logic.
I have a hook that contains asynchronous API requests and it has become quite cumbersome and I have the opportunity to split almost every function of this hook into other hooks, but each of these functions uses a global context (more precisely - dispatch from useReducer()).
So, questions:
Is it ok to use useContext() in every hook who needs it?
How will it affect performance if, for example, I create 10 custom hooks that use useContext() internally and use them in the component.
Example:
providers/Store.js
import React, { createContext, useReducer } from 'react';
export const StoreContext = createContext();
export const StoreProvider = ({ children }) => {
/**
* Store that contains combined reducers.
*/
const store = useReducer(rootReducer, initialState);
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};
hooks/useStore.js
import { useContext } from 'react';
import { StoreContext } from '../providers';
export const useStore = () => useContext(StoreContext);
hooks/useFoo.js
import { useCallback } from 'react';
import { useStore } from './useStore';
export const useFoo = () => {
const [, dispatch] = useStore();
const doFoo = useCallback(
async params => {
dispatch(actions.request());
try {
const res = await SomeService.getSomething(params);
dispatch(actions.add(res));
dispatch(actions.success());
} catch (error) {
dispatch(actions.failure());
}
},
[dispatch]
);
return { doFoo };
};
hooks/useBar.js
import { useCallback } from 'react';
import { useStore } from './useStore';
export const useBar = () => {
const [, dispatch] = useStore();
const doBar = useCallback(
async params => {
dispatch(actions.request());
try {
const res = await SomeService.getSomething(params);
dispatch(actions.success());
dispatch(actions.add(res));
} catch (error) {
dispatch(actions.failure());
}
},
[dispatch]
);
return { doBar };
};
hooks/useNext.js
...
import { useStore } from './useStore';
export const useNext = () => {
const [, dispatch] = useStore();
...
};
components/SomeComponent.js
const SomeComponent = () => {
// use context
const [store, dispatch] = useStore();
// and here is the context
const { doFoo } = useFoo();
// and here
const { doBar } = useBar();
// and here
useNext();
return (
<>
<Button onClick={doFoo}>Foo</Button>
<Button onClick={doBar}>Bar</Button>
// the flag is also available in another component
{store.isLoading && <Spin />}
</>
)
}
Internally, hooks can reference a state queue owned by component. (Under the hood of React’s hooks system - Eytan Manor
)
useContext is just to reference a global state from the relative Context Provider. There is almost no overhead from useContext as you are concerned.