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.
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
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 😍
You can try it on his demo deploy 👆
2: Identify possible states
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.
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.
Yep that's exactly it (or .subscribe(...))
— David K 🎹 (@DavidKPiano) October 8, 2020
You can't directly read state - local, isolated state is a core tenet of the Actor model. But you can "remember" the last emitted state.
For example, you can't know where a hurricane is, but you can know where it was last reported.
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
PS: continue reading with part 2 👉 How to write tests for XState
Continue reading about Refactoring a useReducer to XState, pt1 – CodeWithSwiz 11
Semantically similar articles hand-picked by GPT-4
- How to write tests for XState – CodeWithSwiz 12
- Swap useReducer with XState – CodeWithSwiz 13
- React context without context, using XState – CodeWithSwiz 14, 15
- When your brain is breaking, try Stately.ai
- Reader question: useReducer or XState?
Learned something new? Want to become a React expert?
Learning from tutorials is easy. You follow some steps, learn a smol lesson, and feel like you got dis 💪
Then comes the interview, a real world problem, or a question from the boss. Your mind goes blank. Shit, how does this work again ...
Happens to everyone. Building is harder than recognizing and the real world is a mess! Nothing fits in neat little boxes you learned about in tutorials.
That's where my emails come in – lessons from experience. Building production software :)
Leave your email and get the React email Series - a series of curated essays on building with React. Borne from experience, seasoned with over 12+ years of hands-on practice building the engineering side of growing SaaS companies.
Get Curated React Essays
Get a series of curated essays on React. Lessons and insights from building software for production. No bullshit.
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 ❤️