From f308ba5bbfda327418a87cf50238fa33c489b57d Mon Sep 17 00:00:00 2001 From: Nicholas Romero Date: Tue, 24 Jan 2023 10:41:19 -0600 Subject: [PATCH 1/4] feat: --- ...2023-01-23-react-viewer-context-pattern.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 data/posts/2023-01-23-react-viewer-context-pattern.md diff --git a/data/posts/2023-01-23-react-viewer-context-pattern.md b/data/posts/2023-01-23-react-viewer-context-pattern.md new file mode 100644 index 0000000..5662525 --- /dev/null +++ b/data/posts/2023-01-23-react-viewer-context-pattern.md @@ -0,0 +1,161 @@ +--- +authorId: nromero +categories: + +- remix +- rollup + date: '2023-01-23' + slug: react-viewer-context-pattern + title: Using React context to manage our currently authenticated user's (viewer) state. + +--- + +# Why + +Using [React Context](https://reactjs.org/docs/context.html) allows us to make one call to check the currently +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](https://medium.com/geekculture/props-drilling-v-s-context-api-which-one-is-the-best-75c503d21a65)) + +# What + +```typescript +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(undefined); + +/** + * Return 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. + +```tsx +export default function App() { + const data = useRealAuthenticationCheck(); + return ( + + + + + + + + + + + + ); +} +``` + +Then any where else in our application we can call `useViewer` + +```tsx +export default function HelloName() { + const viewer = useViewer() + return ( +
+

Hello {viewer.firstName}

+ {Steps} +
+ ); +} +``` + +## Authenticated Only Routes/Pages + +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. + +### SSR + +Using a framework like [Remix.js](https://remix.run/) which is always server side rendered we would check on the server +if the user is authenticated before rendering any of the content. + +```typescript +/** + * Ensure user is authenticated, error is thrown to make 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](https://remix.run/docs/en/v1/route/loader) might look like this + +```typescript +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; +} +``` + +### Client Side checks + +In this scenario our viewer context also has loading field and viewer has moved to its own field on the return object. + +```typescript +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 and 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} +}; +``` + +### Final Thoughts + +To some extent the SSR version of `useAuthenticatedViewer` should also redirect in the case that the session cookie expires etc. From 292e05e99e13a713aa15a0f11522556c58188b2a Mon Sep 17 00:00:00 2001 From: Nicholas Romero Date: Tue, 24 Jan 2023 10:44:44 -0600 Subject: [PATCH 2/4] chore: --- ...2023-01-23-react-viewer-context-pattern.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data/posts/2023-01-23-react-viewer-context-pattern.md b/data/posts/2023-01-23-react-viewer-context-pattern.md index 5662525..187d9d8 100644 --- a/data/posts/2023-01-23-react-viewer-context-pattern.md +++ b/data/posts/2023-01-23-react-viewer-context-pattern.md @@ -13,7 +13,7 @@ categories: # Why Using [React Context](https://reactjs.org/docs/context.html) allows us to make one call to check the currently -authenticated user is aka the "Viewer" at the top of our React tree, and we then want to be able to use the viewer +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](https://medium.com/geekculture/props-drilling-v-s-context-api-which-one-is-the-best-75c503d21a65)) @@ -35,7 +35,7 @@ export type Viewer = { export const ViewerContext = React.createContext(undefined); /** - * Return currently authenticated viewer (or undefined if no viewer is defined) + * Return the currently authenticated viewer (or undefined if no viewer is defined) */ export default function useViewer() { return React.useContext(ViewerContext); @@ -63,7 +63,7 @@ export default function App() { } ``` -Then any where else in our application we can call `useViewer` +Any where else in our application we can call `useViewer` ```tsx export default function HelloName() { @@ -84,14 +84,14 @@ authenticated viewer. This encapsulates this logic into reusable hooks, helping ### SSR -Using a framework like [Remix.js](https://remix.run/) which is always server side rendered we would check on the server +Using a framework like [Remix.js](https://remix.run/) which is always server-side rendered we would check on the server if the user is authenticated before rendering any of the content. ```typescript /** - * Ensure user is authenticated, error is thrown to make viewer always be defined + * 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. + * Redirect non-authenticated viewers in the routes Remix Loader. */ export function useAuthenticatedViewer() { const viewer = useViewer(); @@ -101,7 +101,7 @@ export function useAuthenticatedViewer() { ``` 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. +viewer object_ as the server will have redirected any non-authenticated users before rendering this component. A [Remix Loader](https://remix.run/docs/en/v1/route/loader) might look like this @@ -119,7 +119,7 @@ export async function loader({request}: LoaderArgs) { ### Client Side checks -In this scenario our viewer context also has loading field and viewer has moved to its own field on the return object. +In this scenario, our viewer context also has a loading field and the viewer has moved to a field on the returned object. ```typescript const ViewerContext = React.createContext<{ @@ -134,7 +134,7 @@ const isClient = typeof window === "undefined"; /** * Used in pages and components where the route has asserted viewer query is not loading - * and and authenticated viewer was returned. + * and the authenticated viewer was returned. */ export const useAuthenticated = () => { const location = useLocation(); @@ -158,4 +158,4 @@ export const useAuthenticated = () => { ### Final Thoughts -To some extent the SSR version of `useAuthenticatedViewer` should also redirect in the case that the session cookie expires etc. +To some extent, the SSR version of `useAuthenticatedViewer` should also redirect in the case that the session cookie expires etc. From cb61b27f57e37d26dc9142521582685fee9c9477 Mon Sep 17 00:00:00 2001 From: Nicholas Romero Date: Tue, 24 Jan 2023 11:36:04 -0600 Subject: [PATCH 3/4] Update 2023-01-23-react-viewer-context-pattern.md --- data/posts/2023-01-23-react-viewer-context-pattern.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/posts/2023-01-23-react-viewer-context-pattern.md b/data/posts/2023-01-23-react-viewer-context-pattern.md index 187d9d8..f4528b6 100644 --- a/data/posts/2023-01-23-react-viewer-context-pattern.md +++ b/data/posts/2023-01-23-react-viewer-context-pattern.md @@ -2,7 +2,7 @@ authorId: nromero categories: -- remix +- react - rollup date: '2023-01-23' slug: react-viewer-context-pattern From 4e9a585058dc80dc7461188d567ea8be54b18d70 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 27 Jan 2023 13:51:41 -0500 Subject: [PATCH 4/4] Fix spacing --- ...2023-01-23-react-viewer-context-pattern.md | 126 +++++++++--------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/data/posts/2023-01-23-react-viewer-context-pattern.md b/data/posts/2023-01-23-react-viewer-context-pattern.md index f4528b6..5803383 100644 --- a/data/posts/2023-01-23-react-viewer-context-pattern.md +++ b/data/posts/2023-01-23-react-viewer-context-pattern.md @@ -1,32 +1,33 @@ --- authorId: nromero categories: - -- react -- rollup - date: '2023-01-23' - slug: react-viewer-context-pattern - title: Using React context to manage our currently authenticated user's (viewer) state. - + - react + - rollup +date: '2023-01-23' +slug: react-viewer-context-pattern +title: + Using React context to manage our currently authenticated user's (viewer) + state. --- # Why -Using [React Context](https://reactjs.org/docs/context.html) 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](https://medium.com/geekculture/props-drilling-v-s-context-api-which-one-is-the-best-75c503d21a65)) +Using [React Context](https://reactjs.org/docs/context.html) 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](https://medium.com/geekculture/props-drilling-v-s-context-api-which-one-is-the-best-75c503d21a65)) # What ```typescript -import React from "react"; +import React from 'react'; export type Viewer = { - id: string; - email: string; - firstName: string; - lastName: string; + id: string; + email: string; + firstName: string; + lastName: string; }; /** @@ -38,7 +39,7 @@ export const ViewerContext = React.createContext(undefined); * Return the currently authenticated viewer (or undefined if no viewer is defined) */ export default function useViewer() { - return React.useContext(ViewerContext); + return React.useContext(ViewerContext); } ``` @@ -46,20 +47,20 @@ We would initialize our Viewer context at the top of our React tree like so. ```tsx export default function App() { - const data = useRealAuthenticationCheck(); - return ( - - - - - - + const data = useRealAuthenticationCheck(); + return ( + + + + + + - + - - - ); + + + ); } ``` @@ -67,25 +68,27 @@ Any where else in our application we can call `useViewer` ```tsx export default function HelloName() { - const viewer = useViewer() - return ( -
-

Hello {viewer.firstName}

- {Steps} -
- ); + const viewer = useViewer(); + return ( +
+

Hello {viewer.firstName}

+ {Steps} +
+ ); } ``` ## Authenticated Only Routes/Pages -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. +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. ### SSR -Using a framework like [Remix.js](https://remix.run/) which is always server-side rendered we would check on the server -if the user is authenticated before rendering any of the content. +Using a framework like [Remix.js](https://remix.run/) which is always +server-side rendered we would check on the server if the user is authenticated +before rendering any of the content. ```typescript /** @@ -94,43 +97,45 @@ if the user is authenticated before rendering any of the content. * 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; + 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. +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](https://remix.run/docs/en/v1/route/loader) might look like this ```typescript -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; +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; } ``` ### Client Side checks -In this scenario, our viewer context also has a loading field and the viewer has moved to a field on the returned object. +In this scenario, our viewer context also has a loading field and the viewer has +moved to a field on the returned object. ```typescript const ViewerContext = React.createContext<{ - viewer: ViewerQuery["viewer"]; + viewer: ViewerQuery['viewer']; loading: boolean; }>({ viewer: undefined, - loading: false, + loading: false }); -const isClient = typeof window === "undefined"; +const isClient = typeof window === 'undefined'; /** * Used in pages and components where the route has asserted viewer query is not loading @@ -151,11 +156,12 @@ export const useAuthenticated = () => { navigate(`/login`); } }, [loading, navigate, viewer]); - - return {viewer, loading} + + return { viewer, loading }; }; ``` ### Final Thoughts -To some extent, the SSR version of `useAuthenticatedViewer` should also redirect in the case that the session cookie expires etc. +To some extent, the SSR version of `useAuthenticatedViewer` should also redirect +in the case that the session cookie expires etc.