Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    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

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

    Did you enjoy this article?

    Continue reading about React context without context, using XState – CodeWithSwiz 14, 15

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

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

    Created by Swizec with ❤️