
Zustand is goated, but can it replace React context?
I am going to explore a use case for authentication in a React app. I am sure this can apply to Next.js apps as well.
What is Zustand?
It is a state management library that decouples state from a component. This is similar to how Redux and React’s Context API, but is MUCH simpler and lightweight. Redux is HARD to learn and has tons of boilerplate. The Context API is good, but I try to avoid it because I don’t want to have to wrap my App component in many levels of context.
Just a heads up. It is very possible to create authentication with the Context API.
Getting Started
I am going to use Firebase Authentication for simplicity, but remember you can do session or token based validation yourself or other tools.
Let’s first set up our configurations with firebase and export the auth object that we will be using throughout our application.
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
// config
const firebaseConfig = {
apiKey: "YOUR API KEY",
authDomain: "YOUR AUTH DOMAIN",
projectId: "YOUR PROJECT ID",
storageBucket: "YOUR STORAGE BUCKET",
messagingSenderId: "YOUR MESSAGING SENDER ID",
appId: "YOUR APP ID"
};
// initalize the app
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
Creating our auth store
First define our store type. A store is just a hook where we store state. We will include the user, loading state, token, and respective setter methods.
import { create } from 'zustand';
import { User } from 'firebase/auth';
interface IAuth {
user: User | null; //type User comes from firebase
loading: boolean;
token: string | null;
setUser: (user: User | null) => void,
setLoading: (loading: boolean) => void,
setToken: (token: string | null) => void
}
const useAuthStore = create<IAuth>((set) => ({
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
setLoading: (loading) => set({ loading }),
user: null,
token: null,
loading: false,
}));
export default useAuthStore
Are we done?
Let’s try. We can define a sign in method. Great, we can sign in.
However, we need to somehow update token and user when we sign in.
export const firebaseSignIn = async (successCallback: (token: string | undefined) => void) => {
signInWithPopup(auth)
.then(async (result) => {
// can perform an action like hitting login endpoint on your own backend
successCallback(await result.user.getIdToken())
}).catch((error) => {
console.log('error', error)
});
};
Last Steps
Firebase auth offers a onAuthStateChanged event listener. When the app refreshes or the user signs in this listener will fire. Unfortunately, we need to initialize it when the app loads. And even more unfortunately zustand doesn’t seem to have something that can dynamically initialize state because it’s not tied to a component lifecycle!
As a workaround, I wrap my useAuthStore with a useAuth hook.
This is perfect because we can use the onAuthStateChanged event listener from firebase auth to check the auth state and act accordingly.
If a token already existed, aka the user already was signed in, this event listener will fire and set the user and token. It will also fire upon the firebaseSignIn method defined above.
const useAuth = () => {
// accessing state and setters from zustand store we created above
// using react router for navigation to /login if user doesn't exist!
const { user, token, loading, setUser, setToken, setLoading } = useAuthStore();
const navigate = useNavigate()
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (!user) {
// go to login page if user doesn't exist
setLoading(false)
navigate('/login')
} else {
setUser(user)
setToken(await user?.getIdToken())
}
});
return () => {
unsubscribe()
}
}, [])
return { user, token, loading, setLoading }
}
export default useAuth
Woohoo now you have simple and lightweight authentication within your React or Next.js app with zustand that is very modular. Whenever you want to access your token or user all you have to do is use the useAuth hook! Amazing!
Follow for more!