In Refactoring a useReducer to XState, pt1 we talked about a 5 step process to turn a reducer into an XState state machine. Today answers "Ok now what?"
CodeWithSwiz is a twice-a-week live show. Like a podcast with video and fun hacking. Focused on experiments. Join live Wednesdays and Sundays
useAuth relies on the Wormhole state management principle. Heck, that's where I first figured it out!
This has made lots of people very confused. Me included. Come back 6 months later trying to fix a bug and why is there a dispatch({ type: 'startAuthenticating' })
in 5 different places wtf??
"Don't invent new state management schemes, got it" right? No. Don't make your core state messy and confusing 😉
A primer on how useAuth makes wormholes
Wormhole state management gives you global state without the overhead. You add useAuth
to your component and magic happens.
Omg I just opensourced the simplest way to add authentication to your React app.
— Swizec Teller (@Swizec) August 9, 2019
Handles everything for you. Users, login forms, redirects, sharing state between components. Everything
👉 https://t.co/cBUWGp5Fz5
github 👉 https://t.co/eXqB6RerLb
try 👉 https://t.co/tl7IHTufGq pic.twitter.com/eH5lSFIzKq
Internally useAuth
connects to a react context holding your authentication state. Info about the user, their current state, faux cookies, and returns helper methods.
That happens in a couple steps:
// src/AuthProvider.tsx
export const AuthContext = createContext<AuthContextState>({
state: getDefaultState(),
dispatch: () => {},
auth0: null,
callback_domain: "http://localhost:8000",
customPropertyNamespace: "http://localhost:8000",
navigate: (path: string) => {},
})
This creates a new context with default values. state
is the application state we're managing, dispatch
is the changer method, the rest is meta data and configuration. Values we need everywhere that don't change.
// src/AuthReducer.tsx
// Holds authentication state
const [state, dispatch] = useReducer<React.Reducer<AuthState, AuthAction>>(
authReducer,
getDefaultState()
)
const [contextValue, setContextValue] = useState<AuthContextState>({
state,
dispatch,
auth0,
callback_domain: callbackDomain,
customPropertyNamespace,
navigate,
})
// Update context value and trigger re-render
// This patterns avoids unnecessary deep renders
// https://reactjs.org/docs/context.html#caveats
useEffect(() => {
setContextValue((contextValue: AuthContextState) => ({
...contextValue,
state,
}))
}, [state])
The provider instantiates a new reducer and gets access to current state
and a dispatch
method. These and the config metadata are shoved into a new useState
.
We use an effect to update the context value when reducer state
changes. This ensures re-renders happen only when the reducer makes a change.
useAuth
wormholes into this state with a useContext
.
// src/useAuth
export const useAuth: useAuthInterface = () => {
const {
state,
dispatch,
auth0,
callback_domain,
navigate,
customPropertyNamespace
} = useContext(AuthContext);
Anything inside useAuth
now has access to these values. Helper methods can check state
, dispatch
reducer actions, and look at the meta values.
Works great.
How XState helps us improve
Wormhole state worked great but the reducer was problematic. Convoluted to understand and difficult to use.
startAuthenticating
fires twice in the code, so does stopAuthenticating
. Why? Both where it doesn't make sense.
An XState state machine makes the flow clearer 👇
You start as unauthenticated
and trigger LOGIN
. That moves you to authenticating
. From there you can get an ERROR
, which puts you in the error
state. Or you get AUTHENTICATED
and move into the authenticated
state.
LOGOUT
moves you back to unauthenticated
.
Makes sense eh?
While refactoring, we made the state machine closely follow the reducer implementation.
Means we can do a swap!
Change useReducer
to useMachine
from XState
// src/AuthProvider.tsx
// Holds authentication state
const [state, dispatch] = useReducer<React.Reducer<AuthState, AuthAction>>(
authReducer,
getDefaultState()
);
👇
// Holds authentication state
const [state, send] = useMachine(authMachine);
Change what goes into context:
const [contextValue, setContextValue] = useState<AuthContextState>({
state,
dispatch,
👇
const [contextValue, setContextValue] = useState<AuthContextState>({
state: state.context,
send,
Dispatch becomes send and state becomes the XState context. Yes we're overloading terms and that's unfortunate. State machine context is where we keep our application state.
Make the same state
fix in the effect:
useEffect(() => {
setContextValue((contextValue: AuthContextState) => ({
...contextValue,
state
}));
}, [state]);
👇
useEffect(() => {
setContextValue((contextValue: AuthContextState) => ({
...contextValue,
state: state.context
}));
}, [state]);
And then it's a schlep to go around the codebase and change every dispatch({ type: X })
to send(X)
.
We went from 9 dispatch()
calls to 9 send()
calls, which doesn't sound like an improvement. 🤔
But it's because state machines are strict and we used 2 sends to hydrate from local storage. useReducer
let us fake that part.
😅
Next step
David, the creator of XState, says we can stop using a context provider. And that's what we're gonna try next.
Cheers,
~Swizec
PS: continue reading with part 4 👉 React context without context, using XState
Continue reading about Swap useReducer with XState – CodeWithSwiz 13
Semantically similar articles hand-picked by GPT-4
- Refactoring a useReducer to XState, pt1 – CodeWithSwiz 11
- React context without context, using XState – CodeWithSwiz 14, 15
- How to write tests for XState – CodeWithSwiz 12
- useReducer + useContext for easy global state without libraries
- Wormhole state management
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️