User authentication and persistence : Firebase 9, React, Redux Toolkit

I was interested to learn more on how to persist user information through a react application with Firebase authentication (email, password) and Redux. I faced a few challenges but got the pieces together at the end.

Forenotes

We assume here that you already did the following:

  • Setup a Firebase account
  • Install the Firebase CLI
  • Connect your terminal to your Firebase account
  • Created a new app in Firebase
  • Created a react application

Firebase is a Baas (Backend-as-a-Service) built on Google's infrastructure. It offers multiple tools as services for developers such as authentication, realtime database, hosting, messaging services and more. ➡ firebase.google.com In this use case, I only use the authentication service from Firebase.

React (also known as React.js or ReactJS) is a free and open-source front-end JavaScript library for building user interfaces based on UI components. It is maintained by Meta (formerly Facebook) and a community of individual developers and companies. (Wikipedia)

Let's start

In your project src, create a new file firebase.js and copy your Firebase config information.

//src/firebase.js

const firebaseConfig = {
  apiKey: 'xxxxxxxxxxxxxxxxxxxx',
  authDomain: 'xxxxxxxxxxxxxxxxxxxx',
  projectId: 'xxxxxxxxxxxxxxxxxxxx',
  storageBucket: 'xxxxxxxxxxxxxxxxxxxx',
  messagingSenderId: 'xxxxxxxxxxxxxxxxxxxx',
  appId: 'xxxxxxxxxxxxxxxxxxxx',
};

Initialize your application and import the authentication components from firebase/auth We put all our import in this file so it makes it easier to use the different parts in the rest of our application.

//src/firebase.js
import { initializeApp } from 'firebase/app';
import { 
getAuth, 
createUserWithEmailAndPassword, 
updateProfil, 
onAuthStateChanged, 
signInWithEmailAndPassword, 
signOut 
} from 'firebase/auth';
...
// your firebase config here
...
//init firebase app
initializeApp(firebaseConfig);

//init services
const auth = getAuth();

export {
auth,
createUserWithEmailAndPassword,
updateProfile,
onAuthStateChanged,
signInWithEmailAndPassword,
signOut
}

Redux setup

Redux gives us the ability to centralize our application state and logic. It allows us to share the state between the different components of our application and setup a process on how components can interact with the store to read or update the state tree of the application.

Make sure you have the necessary packages installed

# NPM
npm install @reduxjs/toolkit react-redux

# Yarn
yarn add @reduxjs/toolkit react-redux

Redux store

A store is the complete state tree of the application. It is a state container holding the application’s state. Redux can have only a single store in an application. When a store is created in Redux, a reducer has to be specified. There is only one way to change the state inside the store; it is to dispatch an action on it.

A store is an object with a few methods that we will need for our use case.

Store methods we will use

getState()
It returns the current state tree of the application

dispatch(action)
This is the only way to mutate the state of the application. The store will be called with the current getState() result and an action passed with dispatch. The returned value is considered to be next state. The store use its reducing function with the current state and the given action to return a new state.

A reducing function or reducer, is a function that accepts an accumulation and a value and returns a new accumulation. They are used to reduce a collection of values down to a single value. In our case the returning value is the new state object.

Reducer documentation on MDN

for more information about the redux store : redux.js.org/api/store

Start by creating the store in your application.

//src/app/store.js

import { configureStore } from '@reduxjs/toolkit';
import userReducer from '../features/userSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

Slice

By the Redux documentation, a "slice" is a collection of Redux reducer logic and actions for a single feature in your app. Usually slices are a single file.
We split up the root Redux state into multiple smaller slices of state for each feature of the application.
In our case we will have a userSlice file where we will have the reducer's actions and logic for the user login and logout. The state will be a user when logged in and null when logged out.

Create your userSlice.js file features/userSlice.js

// features/userSlice.js

import { createSlice } from '@reduxjs/toolkit';

export const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
  },
  reducers: {
    login: (state, action) => {
      state.user = action.payload;
    },
    logout: (state) => {
      state.user = null;
    },
  },
});

export const { login, logout } = userSlice.actions;

// selectors
export const selectUser = (state) => state.user.user;

export default userSlice.reducer;

Implement in your application file

Import the necessary elements from react-redux in your App.js file. Import as well the necessary elements for your user authentication.

//App.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { login, logout, selectUser } from './features/userSlice';
import { auth, onAuthStateChanged } from './firebase';
...

Still in your App.js file initialize two constants for your user and for your dispatch (the information you will send to the redux state).

onAuthStateChanged is an observer on the firebase auth object. It takes the auth object in parameters and returns an authenticated user object. If we get a user back, we send its information on the state with our store method dispatch and the action login.

function App() {
  const user = useSelector(selectUser);
  const dispatch = useDispatch();

// check at page load if a user is authenticated
  useEffect(() => {
    onAuthStateChanged(auth, (userAuth) => {
      if (userAuth) {
        // user is logged in, send the user's details to redux, store the current user in the state
        dispatch(
          login({
            email: userAuth.email,
            uid: userAuth.uid,
            displayName: userAuth.displayName,
            photoUrl: userAuth.photoURL,
          })
        );
      } else {
        dispatch(logout());
      }
    });
  }, []);

Application components

My applications is structured this way :

//App.js
...
return (
    <div className='app'>
      <Header />

      // check if a user is logged in
      {!user ? (
        // display the login form 
        <Login />
      ) : (
        // display the rest of the app
        <div className='app__body'>
          {/* Rest of the app */}
        </div>
      )}
    </div>
  );
}

export default App;

The login component

import React, { useState } from 'react';
import {
  auth,
  createUserWithEmailAndPassword,
  updateProfile,
  signInWithEmailAndPassword,
} from './firebase';
import { useDispatch } from 'react-redux';
import { login } from './features/userSlice';

function Login() {
// use state constants for the the form inputs
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [profilePic, setProfilePic] = useState('');
  const dispatch = useDispatch();

  const loginToApp = (e) => {
    e.preventDefault();

    // Sign in an existing user with Firebase
    signInWithEmailAndPassword(auth, email, password)
    // returns  an auth object after a successful authentication
    // userAuth.user contains all our user details
      .then((userAuth) => {
      // store the user's information in the redux state
        dispatch(
          login({
            email: userAuth.user.email,
            uid: userAuth.user.uid,
            displayName: userAuth.user.displayName,
            photoUrl: userAuth.user.photoURL,
          })
        );
      })
// display the error if any
      .catch((err) => {
        alert(err);
      });
  };

// A quick check on the name field to make it mandatory
  const register = () => {
    if (!name) {
      return alert('Please enter a full name');
    }

    // Create a new user with Firebase
    createUserWithEmailAndPassword(auth, email, password)
      .then((userAuth) => {
      // Update the newly created user with a display name and a picture
        updateProfile(userAuth.user, {
          displayName: name,
          photoURL: profilePic,
        })
          .then(
            // Dispatch the user information for persistence in the redux state
            dispatch(
              login({
                email: userAuth.user.email,
                uid: userAuth.user.uid,
                displayName: name,
                photoUrl: profilePic,
              })
            )
          )
          .catch((error) => {
            console.log('user not updated');
          });
      })
      .catch((err) => {
        alert(err);
      });
  };

  return (
    <div>
      <div className='login'>
        <form>
          <input
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder='Full name (required for registering)'
            type='text'
          />

          <input
            value={profilePic}
            onChange={(e) => setProfilePic(e.target.value)}
            placeholder='Profile picture URL (optional)'
            type='text'
          />
          <input
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder='Email'
            type='email'
          />
          <input 
            value={password} 
            onChange={(e) => setPassword(e.target.value)}
            placeholder='Password'
            type='password'
          />
          <button type='submit' onClick={loginToApp}>
            Sign In
          </button>
        </form>

        <p>
          Not a member?{' '}
          <span className='login__register' onClick={register}>
            Register Now
          </span>
        </p>
      </div>
    </div>
  );
}

export default Login;

You can add a logout function in your header for example like this

import { useDispatch, useSelector } from 'react-redux';
import { auth } from './firebase';
import { logout, selectUser } from './features/userSlice';

function Header() {
  const dispatch = useDispatch();

  const logoutOfApp = () => {
    // dispatch to the store with the logout action
    dispatch(logout());
    // sign out function from firebase
    auth.signOut();
  };

  const user = useSelector(selectUser);

  return (
    <div className='header'>
         ...
        <button onClick={logoutOfApp}>Logout</button>
         ...
    </div>
  );
}

export default Header;

GitHub Repository

I compiled the example discussed in this article in a react project that you can check on GitHub.
Update the file firebase_example.js with your firebase configuration infos and rename the file in firebase.js.

Notes

This is my personal notes on trying to connect the dots between Firebase 9, a react application and Redux Toolkit. I am not an expert in any of these and the code found here probably doesn't follow the best practices, but the purpose of these notes is to document my personal experience and challenges met. Let me know in the comments if this information has been helpful share your insights if you see some useful improvement I could bring to my solution.