Skip to content
Swizec Teller - a geek with a hatswizec.com

Refactoring a useReducer to XState, pt1 – CodeWithSwiz 11

XState promises to be like useReducer+Context combined and the simplest way to handle complex state in React apps. But can it deliver? On this episode of #CodeWithSwiz, we gave it a shot.

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 was always meant to become the best way to add authentication to your React and JAMStack apps. Regardless of auth provider.

But as time passed, contributors grew (up to 19), pull requests mounted (70 closed or merged 🤘), and bugs got fixed, the codebase became a mess. Hard to reason about, difficult to understand, bleh to work with.

It was time to refactor. And When your brain is breaking, try XState

1: Housework

We started in ep10 with housework.

Merge existing PRs, verify tests work, fix lingering bugs. Anything that could lead to merge conflicts after a large change to the codebase.

You don't want to leave your contributors hanging.

Among the neglected pull requests we found a fantastic candidate for a useAuth docs page! Thanks Eric Hodges, you're amazing 😍

kHQSi9pngi2389a

You can try it on his demo deploy 👆

2: Identify possible states

XState visualization of the useAuth state machine
XState visualization of the useAuth state machine

Refactoring a useReducer to XState starts by identifying your states. Each case corresponds to a transition between states.

export const authReducer = (
state: AuthState,
action: AuthAction
): AuthState => {
switch (action.type) {
case "login":
// ...
case "logout":
// ...
case "stopAuthenticating":
// ...
case "startAuthenticating":
// ...
case "error":
// ...
default:
return state;
}
};

Your XState state machine will have N-1 states.

const authMachine = Machine<AuthState>(
{
id: "useAuth",
initial: "unauthenticated",
// ...
states: {
unauthenticated: {},
authenticating: {},
authenticated: {},
error: {},
},
}
// ...
);

A user can be either unauthenticated, authenticating, authenticated, or there's an error. No other state exists.

3: Define transitions between states

You can read transitions from the reducer. Each case becomes an XState transition.

Harder is identifying what they transition from and to. Reducers hide that info in the gaggle of returned state. You have to read the code and understand its intent.

states: {
unauthenticated: {
on: {
LOGIN: "authenticating"
}
},
authenticating: {
on: {
ERROR: "error",
AUTHENTICATED: "authenticated"
},
},
authenticated: {
on: {
LOGOUT: "unauthenticated"
}
},
error: {}
}

If you're unauthenticated and trigger LOGIN, you get to authenticating. From there you can either get AUTHENTICATED or an ERROR. To get out of the authenticated state, you have to LOGOUT.

Isn't that more readable?

XState's visualizer even draws helpful diagrams.

XState visualization of the useAuth state machine
XState visualization of the useAuth state machine

4: Move your state into context

Final step is moving what you used to think of as "state" into the state machine context.

You can think of context as the meta state. The application state that your state machine states correspond to.

Best copied from your TypeScript definition of the reducer's state ✌️

const authMachine = Machine<AuthState>(
{
// ...
context: {
user: {},
expiresAt: null,
authResult: null,
isAuthenticating: false,
error: undefined,
errorType: undefined
},

XState docs call this the infinite state next to the finite state of a state machine. I like "application state".

This is where you store the values that you care about.

On stream we got as far as manipulating the isAuthenticating flag as a side-effect of the authenticating state. Might be unnecessary now that's an explicit state, but made for a good playground.

const authMachine = Machine<AuthState>(
{
// ...
states: {
// ...
authenticating: {
on: {
ERROR: "error",
AUTHENTICATED: "authenticated",
},
entry: ["startAuthenticating"],
exit: ["stopAuthenticating"],
},
// ...
},
},
{
actions: {
startAuthenticating: assign((context) => {
return {
isAuthenticating: true,
};
}),
stopAuthenticating: assign((context) => {
return {
isAuthenticating: false,
};
}),
},
}
);

Adding entry and exit effects to the AUTHENTICATING action tells XState to call the startAuthenticating and stopAuthenticating actions.

They're set up with the assign() action creator from XState. I figured out this exact incantation off stream. Docs were confusing as heck on stream and making it play with TypeScript was hell.

5: Accessing XState context to read application state

You can't access context directly.

That means you'll have to either .subscribe to changes or use a dirty side effect in an .onTransition call.

Here's a test that works to verify the authenticating entry side-effect.

it("changes isAuthenticating to true", () => {
let context = { isAuthenticating: false };
authMachine
.onTransition((state) => {
context = state.context;
})
.send("LOGIN");
expect(context.isAuthenticating).toBe(true);
});

Notice how I'm storing a value in a variable when onTransition runs. That can get messy and I'm not sure yet how we'll use it to make the authMachine a drop-in replacement for authReducer.

Want to make this refactor invisible to users of useAuth 😊

Fin

In conclusion, XState is like an inverted useReducer. Cases become transitions, the space between cases becomes state machine states, reducer state becomes context.

Cheers,
~Swizec

Did you enjoy this article?

Published on October 8th, 2020 in CodeWithSwiz, Livecoding, Technical, XState

Learned something new?
Want to become a high value JavaScript expert?

Here's how it works 👇

Leave your email and I'll send you an Interactive Modern JavaScript Cheatsheet 📖right away. After that you'll get thoughtfully written emails every week about React, JavaScript, and your career. Lessons learned over my 20 years in the industry working with companies ranging from tiny startups to Fortune5 behemoths.

Start with an interactive cheatsheet 📖

Then get thoughtful letters 💌 on mindsets, tactics, and technical skills for your career.

"Man, love your simple writing! Yours is the only email I open from marketers 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. 👌"

~ Ashish Kumar

Join over 10,000 engineers just like you already improving their careers with my letters, workshops, courses, and talks. ✌️

Have a burning question that you think I can answer? I don't have all of the answers, but I have some! Hit me up on twitter or book a 30min ama for in-depth help.

Ready to Stop copy pasting D3 examples and create data visualizations of your own?  Learn how to build scalable dataviz components your whole team can understand with React for Data Visualization

Curious about Serverless and the modern backend? Check out Serverless Handbook, modern backend for the frontend engineer.

Ready to learn how it all fits together and build a modern webapp from scratch? Learn how to launch a webapp and make your first 💰 on the side with ServerlessReact.Dev

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 ❤️