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.
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
Continue reading about React context without context, using XState – CodeWithSwiz 14, 15
Semantically similar articles hand-picked by GPT-4
- Swap useReducer with XState – CodeWithSwiz 13
- useReducer + useContext for easy global state without libraries
- Refactoring a useReducer to XState, pt1 – CodeWithSwiz 11
- How to write tests for XState – CodeWithSwiz 12
- [CodeWithSwiz 21] useAuth beta support for Firebase 🎉
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 ❤️