Created
July 21, 2023 17:52
-
-
Save jckw/ec0a024907c255691f060a35cec7d2a5 to your computer and use it in GitHub Desktop.
A simple and good auth context with an xstate state machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useActor, useInterpret } from "@xstate/react" | |
import { assign, createMachine, InterpreterFrom } from "xstate" | |
import { createContext, useContext, useEffect, ReactNode } from "react" | |
import apiClient from "@/api/client" | |
import { useRouter } from "next/router" | |
import { useToast } from "@chakra-ui/react" | |
// Warning, if you are altering this machine, you will probably need to manually refresh | |
// the page. Because it is defined outside of React, it will not hot reload. | |
const authMachine = createMachine( | |
{ | |
id: "auth", | |
initial: "checking", | |
context: { | |
user: null, | |
error: null, | |
}, | |
states: { | |
checking: { | |
invoke: { | |
src: "checkAuth", | |
onDone: { | |
target: "authenticated", | |
actions: assign({ user: (context, event) => event.data }), | |
}, | |
onError: "unauthenticated", | |
}, | |
}, | |
authenticated: { | |
on: { | |
LOGOUT: { | |
target: "unauthenticated", | |
actions: "logout", | |
}, | |
}, | |
}, | |
unauthenticated: { | |
on: { | |
LOGIN: "loggingIn", | |
}, | |
}, | |
loggingIn: { | |
invoke: { | |
src: "login", | |
onDone: { | |
target: "authenticated", | |
actions: [ | |
assign({ user: (_context, event) => event.data }), | |
assign({ error: () => null }), | |
], | |
}, | |
onError: { | |
target: "unauthenticated", | |
actions: [ | |
assign({ user: () => null }), | |
assign({ error: (_context, event) => event.data }), | |
], | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
services: { | |
checkAuth: async () => { | |
// Call API to check if user is authenticated or check JWT expiration etc. | |
try { | |
const { uuid } = await apiClient.auth.user.me.execute() | |
return uuid | |
} catch (e) { | |
throw new Error("User not authenticated") | |
} | |
}, | |
login: async (_ctx, event) => { | |
if (event.type !== "LOGIN") throw new Error("Invalid event") | |
const { email, password } = event | |
try { | |
const { uuid, token } = await apiClient.auth.user.login.execute({ | |
email, | |
password, | |
}) | |
localStorage.setItem("@token", token!) | |
return uuid | |
} catch (e: unknown) { | |
throw new Error((e as any).body[0].message) | |
} | |
}, | |
}, | |
actions: { | |
logout: () => { | |
localStorage.removeItem("@token") | |
}, | |
}, | |
} | |
) | |
// Create a context for your service | |
const AuthContext = createContext<InterpreterFrom<typeof authMachine>>( | |
null as any | |
) | |
interface AuthProviderProps { | |
children: ReactNode | |
} | |
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { | |
const authService = useInterpret(authMachine) | |
const router = useRouter() | |
useEffect(() => { | |
const subscription = authService.subscribe((state) => { | |
console.log("Current state: ", state.value) | |
if (state.value === "unauthenticated" && router.pathname !== "/login") { | |
localStorage.removeItem("@token") | |
router.push("/login") | |
} | |
}) | |
return subscription.unsubscribe | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [authService]) | |
return ( | |
<AuthContext.Provider value={authService}>{children}</AuthContext.Provider> | |
) | |
} | |
export const useAuth = () => { | |
const authService = useContext(AuthContext) | |
const [state] = useActor(authService) | |
const toast = useToast() | |
if (authService === null) { | |
throw new Error("useAuth must be used within an AuthProvider") | |
} | |
useEffect(() => { | |
if (state.context.error) { | |
toast({ | |
title: "Error", | |
description: (state.context.error as any).message, | |
status: "error", | |
duration: 5000, | |
isClosable: true, | |
}) | |
} | |
}, [state.context.error, toast]) | |
const login = (email: string, password: string) => { | |
return authService.send({ type: "LOGIN", email, password }) | |
} | |
const logout = () => { | |
authService.send({ type: "LOGOUT" }) | |
} | |
return { state, login, logout } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment