Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Authentication with xState 5, Firebase and Next.js App Router
Georgi Todorov
Georgi Todorov

Posted on • Edited on • Originally published atz-lander.hashnode.dev

Authentication with xState 5, Firebase and Next.js App Router

TL;DR

If you just want to see the code, it ishere. You can also have a look at the xState 4 implementationhere.

Background

In our team, we've been using xState and Firebase for a couple of years to develop a mobile app, and we are naturally continuing with the same stack for the web app. I’ve already started sharing myexperience with React Native and I’m planning to do the same with Next.js. To start with the authentication seems like the obvious choice.

Use case

To experiment with the authentication capabilities of the stack, we will build a simple website with two pages: aSign In screen and aDashboard page accessible only to authenticated users.

Disclaimers

Integrating Firebase in Next.js is quite straightforward and covered in enough online materials, so we won't discuss it in this post. The focus will be on integrating both technologies with xState. Just for clarity, here's the Firebase config file:

import{initializeApp,getApps}from"firebase/app";constfirebaseConfig={apiKey:process.env.NEXT_PUBLIC_FIREBASE_API_KEY,authDomain:process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,projectId:process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,storageBucket:process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,messagingSenderId:process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,appId:process.env.NEXT_PUBLIC_firebaseApp_ID,};letfirebaseApp=getApps().length===0?initializeApp(firebaseConfig):getApps()[0];exportdefaultfirebaseApp;
Enter fullscreen modeExit fullscreen mode

Also, to simplify things further, we will use thesignInAnonymously method, which works the same way as authenticating withemail/password orphone but doesn't require user input.

Implementation

xState machine

Usually, when working with xState and React, I reach a point where I need a globally accessiblemachine. That's why I start by creating anappMachine and pass it to thecreateActorContext method so that it can be used from the React context. This way, we can keep the authentication logic in a single place and send events to/from any page.

Key parts of the machine are the rootGO_TO_AUTHENTICATED andGO_TO_UNAUTHENTICATED events. They lead the user to the correct state and, respectively, to the correct screen.

TheuserSubscriber actor (which will be explained in a bit) plays the role of an orchestrator, which is in charge of listening for the user object from Firebase and targeting the appropriate state with the one of the already mentioned events.

The machine consists of three main states.loading is the initial state that is active until we know the user's status. After that, we transition to one of the other two states -authenticated orunauthenticated. They both have substates and areidle initially. When the user isauthenticated, they can call theSIGN_OUT event to transition to thesigningOut substate, which is in charge of invoking thesignOut method. Theunauthenticated structure is similar, with the difference that instead of signing out, it contains the signing-in logic.

importReact,{PropsWithChildren}from"react";import{setup}from"xstate";import{createActorContext}from"@xstate/react";constappMachine=setup({// machine options},}).createMachine({invoke:{src:"userSubscriber"},on:{GO_TO_AUTHENTICATED:{target:".authenticated"},GO_TO_UNAUTHENTICATED:{target:".unauthenticated"},},initial:"loading",states:{loading:{tags:"loading"},authenticated:{on:{SIGN_OUT:{target:".signingOut"}},initial:"idle",states:{idle:{},signingOut:{invoke:{src:"signOut"},onDone:{target:"idle"},},},},unauthenticated:{on:{SIGN_IN:{target:".signingIn"}},initial:"idle",states:{idle:{},signingIn:{invoke:{src:"signIn"},onDone:{target:"idle"}},},},},});exportconstAppContext=createActorContext(appMachine);exportfunctionAppProvider({children}:PropsWithChildren<{}>){return<AppContext.Provider>{children}</AppContext.Provider>;}
Enter fullscreen modeExit fullscreen mode

Firebase

In order to retrieve the current user, Firebase recommends using theonAuthStateChanged observer, which is a perfect fit for acallback actor. In the callback, we just have to check theuser value. If it isnull, the user is unauthenticated; otherwise, we trigger theGO_TO_AUTHENTICATED event.

For thesignIn actor, as mentioned before, we go with thesignInAnonymously method, and for thesignOut actor, we resolve theauth.signOut() promise. Both of these will reflect on theuser that is being observed in theuserSubscriber service.

import{fromCallback,fromPromise,setup}from"xstate";import{onAuthStateChanged,getAuth,signInAnonymously}from"firebase/auth";importfirebaseAppfrom"@/firebase";constauth=getAuth(firebaseApp);constappMachine=setup({types:{events:{}as|{type:"GO_TO_AUTHENTICATED"}|{type:"GO_TO_UNAUTHENTICATED"}|{type:"SIGN_IN"}|{type:"SIGN_OUT"},},actors:{userSubscriber:fromCallback(({sendBack})=>{constunsubscribe=onAuthStateChanged(auth,(user)=>{if(user){sendBack({type:"GO_TO_AUTHENTICATED"});}else{sendBack({type:"GO_TO_UNAUTHENTICATED"});}});return()=>unsubscribe();}),signIn:fromPromise(async()=>{awaitsignInAnonymously(auth);}),signOut:fromPromise(async()=>{awaitauth.signOut();}),},}).createMachine({// machine definition},});
Enter fullscreen modeExit fullscreen mode

Next.js

From here, we can continue by consuming the context. Wrapping thechildren withAppProvider in theRootLayout gives access to the global machine from all layouts and pages.

import{AppProvider}from"@/contexts/app";import{Loader}from"@/components/Loader";import{StateRouter}from"@/components/StateRouter";constinter=Inter({subsets:["latin"]});exportdefaultfunctionRootLayout({children,}:{children:React.ReactNode;}){return(<htmllang="en"><bodyclassName={inter.className}><AppProvider><Loader>{children}</Loader><StateRouter/></AppProvider></body></html>);}
Enter fullscreen modeExit fullscreen mode

The purpose of the<Loader> component is to prevent pages from rendering before the user data is loaded. TheAppContext.useSelector() hook always updates on state changes, and when the state isloading, we just display a placeholder screen.

"use client";import{PropsWithChildren}from"react";import{AppContext}from"@/contexts/app";exportfunctionLoader({children}:PropsWithChildren<{}>){conststate=AppContext.useSelector((snapshot)=>{returnsnapshot;});returnstate.matches("loading")?(<main><div>Loading...</div></main>):(children);}
Enter fullscreen modeExit fullscreen mode

We handle the actual navigation in theStateRouter component. It is inspired from therouter events example in the Next.js documentation. We listen for changes in theapp machine state and once one of theauthenticated orunauthenticated states is active, the corresponding page will be loaded. If the user already exists, they will be navigated to the dashboard, which is located at the root -/. Otherwise, they should be redirected to the/sign-in page.

"use client";import{useEffect}from"react";import{useRouter}from"next/navigation";import{AppContext}from"@/contexts/app";exportfunctionStateRouter(){constrouter=useRouter();conststate=AppContext.useSelector((snapshot)=>{returnsnapshot;});useEffect(()=>{if(state.matches("unauthenticated")){router.push("/sign-in");}elseif(state.matches("authenticated")){router.push("/");}},[state.value]);returnnull;}
Enter fullscreen modeExit fullscreen mode

In my experience, integrating xState with the navigation lifecycle of another framework is one of the most challenging aspects when setting up the initial application architecture. While this approach may not be the most scalable solution, it works well in terms of code separation.

Conclusion

From the little that I tried out, I'm satisfied with the results so far, but I still have concerns about how the application will grow when adding more pages and interactions with Firebase.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software developer.
  • Location
    Sofia, Bulgaria
  • Joined

More fromGeorgi Todorov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp