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

React context without context, using XState – CodeWithSwiz 14, 15

useAuth works without React Context! πŸŽ‰

CodeWithSwiz is a twice-a-week live show. Like a podcast with video and fun hacking. Focused on experiments. Join live Wednesdays and Sundays

This was quite the saga my friend. Started on October 8th, thought, "Pfft, how hard can it be", and here we are a month later. πŸ˜…

I made a pull request with the full diff, if you're curious. Read about steps 1 through 3 πŸ‘‰ Refactoring a useReducer to XState, Writing tests for XState, Swap useReducer with XState.

Today's post is about the last part: Tossing react context.

Why React Context

React Context is the workhorse behind wormhole state management – the idea that hooks should feel like magic. Call useAuth anywhere in any component and it knows everything about the user.

function RandomComponent() {
const { user } = useAuth()
return <h2>Hello {user.username}</h2>
}

That's what an API in modern React should look like. No prop drilling, no fumbling with global state, no worries about performance. Call the hook, get the info.

useAuth achieves that with context.

A global provider wraps your React tree. It holds a piece of global state and shares it with everyone via context. useAuth hooks into this state with a useContext call.

Like this example from the Wormhole state management article:

A couple counters sharing state via context. You get a neat API.

const { count, incB } = useSharedCount()

But the machinery to make this work is gnarly.

Avoiding unnecessary re-renders involves 2 sets of state. One for context, one for machinery around context. You have to be careful how context updates.

For useAuth, it looked like this:

export const AuthProvider: AuthProviderInterface = ({ ... }) => {
// omitted config code
// 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]);
// omitted auth check code
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
};

Not intimidating at all πŸ˜…

Functions inside useAuth change state via methods they grab out of context, effect in provider sees the change, updates context, triggers re-renders.

Works great and means users have to wrap their code in <AuthProvider>.

You know what would make useAuth feel more magical? No global provider 😍

Using XState over Context

XState introduces the concept of actors – independent objects that hold their own state and respond to events.

Every interpreted XState state machine is an actor. The state machine defines how the actor responds to events, the object is the actor.

import { interpret } from "xstate"
export const authService = interpret(authMachine)
authService.start()

Because you export an instance of an object, you can access that instance anywhere. Your bundler ensures it's the same instance everywhere.

import { authService } from 'authService.ts' doesn't call interpret(authMachine). It returns the memoized result. How that works depends on the bundler you're using.

What that means for you is that you now have global shared state without a context provider.

Thanks to XState's React integration, you can safely access this shared state.

export const useAuth: useAuthInterface = () => {
const [state, eventSend] = useService(authService)
// ...
}

state is the state machine state – unauthenticated, authenticating, authenticated, error. The application state lives in state.context. You can read more about that in Refactoring a useReducer to XState.

eventSend lets you dispatch events to transition between states. LOGIN to start authenticating, LOGOUT to logout, etc. Available transitions depend on the current state of the state machine.

useAuth state machine
useAuth state machine

When state updates, entry and exit actions update state.context. When your components depend on those values, they re-render.

What about configuration?

A common pattern is to use the global context provider as an API for configuration. I used the same approach in useAuth.

export const wrapRootElement = ({ element }) => (
<AuthProvider
navigate={navigate}
auth0_domain="useauth.auth0.com"
auth0_client_id="GjWNFNOHq1ino7lQNJBwEywa1aYtbIzh"
>
{element}
</AuthProvider>
)

Without a provider, how do users configure your library?

With a new event!

I kept the <AuthProvider> as a convenience. Accepts configuration params and passes them into the XState machinery. You can do it without the provider.

export const AuthProvider: AuthProviderInterface = ({ ... }) => {
// omitted config munching
const { dispatch } = useAuth();
useEffect(() => {
const auth0 = new Auth0.WebAuth({ ...params, ...auth0_params });
dispatch("SET_CONFIG", {
authProvider: auth0,
navigate,
customPropertyNamespace,
callbackDomain
});
}, [navigate, customPropertyNamespace, callbackDomain]);
return <React.Fragment>{children}</React.Fragment>;
};

Notice the provider is using the hook. Like any other component would. 🀯

The SET_CONFIG event configures useAuth. And in a move towards supporting many auth providers, the library now accepts a configured auth provider object.

🀘

Cheers, ~Swizec

PS: the React.Fragment bit is interesting, components can't return children without wrapping in a React element. Who knew

Did you enjoy this article?

Published on November 10th, 2020 in CodeWithSwiz, Technical, XState, Livecoding

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