| authorId | nromero | ||
|---|---|---|---|
| categories |
|
||
| date | 2023-01-23 | ||
| slug | react-viewer-context-pattern | ||
| title | Using React context to manage our currently authenticated user's (viewer) state. |
Using React Context allows us to make one call to check the currently the authenticated user is aka the "Viewer" at the top of our React tree, and we then want to be able to use the viewer object throughout our application without passing it around as a prop (Prop Drilling Vs Context)
import React from 'react';
export type Viewer = {
id: string;
email: string;
firstName: string;
lastName: string;
};
/**
* Viewer context will always return either undefined or the viewer type.
*/
export const ViewerContext = React.createContext<Viewer | undefined>(undefined);
/**
* Return the currently authenticated viewer (or undefined if no viewer is defined)
*/
export default function useViewer() {
return React.useContext(ViewerContext);
}We would initialize our Viewer context at the top of our React tree like so.
export default function App() {
const data = useRealAuthenticationCheck();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<ViewerContext.Provider value={data.viewer}>
<Outlet />
</ViewerContext.Provider>
</body>
</html>
);
}Any where else in our application we can call useViewer
export default function HelloName() {
const viewer = useViewer();
return (
<div>
<h1>Hello {viewer.firstName}</h1>
<Stepper>{Steps}</Stepper>
</div>
);
}The next few snippets will demonstrate how we can handle components that should only ever be rendered with an authenticated viewer. This encapsulates this logic into reusable hooks, helping us avoid rewriting this logic per route.
Using a framework like Remix.js which is always server-side rendered we would check on the server if the user is authenticated before rendering any of the content.
/**
* Ensure the user is authenticated, an error is thrown to make the viewer always be defined
* in any code after this hook from the type checkers perspective.
* Redirect non-authenticated viewers in the routes Remix Loader.
*/
export function useAuthenticatedViewer() {
const viewer = useViewer();
if (!viewer) throw new Error('Authenticated viewer is required.');
return viewer;
}Thus the useAuthenticatedViewer hook throws an error to let the type checker
know that this hook will always return a viewer object as the server will have
redirected any non-authenticated users before rendering this component.
A Remix Loader might look like this
export async function loader({ request }: LoaderArgs) {
const viewer = await checkBackendForUser();
if (!viewer) {
// Redirect to the home page if they are already signed in.
return redirect('/login');
}
return null;
}In this scenario, our viewer context also has a loading field and the viewer has moved to a field on the returned object.
const ViewerContext = React.createContext<{
viewer: ViewerQuery['viewer'];
loading: boolean;
}>({
viewer: undefined,
loading: false
});
const isClient = typeof window === 'undefined';
/**
* Used in pages and components where the route has asserted viewer query is not loading
* and the authenticated viewer was returned.
*/
export const useAuthenticated = () => {
const location = useLocation();
const navigate = useNavigate();
const { viewer, loading } = useViewer();
/**
* Only redirect
* 1. We are on client
* 2. if useViewer request is not loading
* 3. viewer is not authenticated.
*/
React.useEffect(() => {
if (isClient && !loading && !viewer) {
navigate(`/login`);
}
}, [loading, navigate, viewer]);
return { viewer, loading };
};To some extent, the SSR version of useAuthenticatedViewer should also redirect
in the case that the session cookie expires etc.