DEV Community

Cover image for Hire+Plus! For Employees Here's how I built it (Redux - Auth)
AjeaS
AjeaS

Posted on • Updated on

Hire+Plus! For Employees Here's how I built it (Redux - Auth)

Purpose: The candidate will be able to login, login with google, sign up, and logout.

I'm using Redux as my state management, more specifically Redux toolkit. A package for making working with redux much simpler than before. You can read more about the details of Redux Toolkit here.

I added it to my project using npm
npm install @reduxjs/toolkit @react-redux

I followed the instructions on the docs (linked above) to set it up.

I created an app folder where all my redux code will live, separating my reducers as features related to the project (auth, profile, job company).
Redux folder structure

Configure Redux toolkit

store.ts - configure redux and my reducers

import { configureStore } from '@reduxjs/toolkit';
import authReducer from './features/auth/authSlice';
import profileReducer from './features/profile/profileSlice';
import jobReducer from './features/job/jobSlice';
import companyReducer from './features/company/companySlice';

export const store = configureStore({
  reducer: {
     auth: authReducer,
     profile: profileReducer,
     job: jobReducer,
     company: companyReducer,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;

// Inferred type: {users: UsersState}
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode

RootState and AppDispatch are Redux toolkit's version of using useSelector and useDispatch.

hooks.ts - export redux toolkit's typed version of state and action hooks.

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Enter fullscreen mode Exit fullscreen mode

index.tsx - Pass store provider to the entire app

import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import 'tw-elements';
import './index.css';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
 <BrowserRouter>
   <Provider store={store}>
     <App />
   </Provider>
 </BrowserRouter>
);
Enter fullscreen mode Exit fullscreen mode

Types, Actions and Reducers: Auth state

Types

inside app > features > auth > authTypes.ts
Data types for my login info and signup info.

export type LoginFields = {
 email: string;
 password: string;
};
export type SignUpFields = {
 displayName: string;
 email: string;
 password: string;
};
Enter fullscreen mode Exit fullscreen mode

Actions

inside app > features > auth > authSlice.ts
setting the initial state for auth reducer, using signInWithGoogle, signInWithEmailAndPassword, signUpUserEmailAndPassword, and signoutUser funcs from DB.

signInWithEmailAndPassword and signUpUserEmailAndPassword both return stringifyed version of auth user.

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

import {
 signInWithGooglePopup,
 signInEmailAndPassword,
 signUpEmailAndPassword,
 logoutUser,
} from '../../../utils/firebase/firebase.utils';
import { SignUpFields, LoginFields } from './authTypes';

interface authState {
 isSignedIn: boolean;
 currentUser: { uid: string; displayName: string };
 isLoading: boolean;
 signUpError: string;
 signInError: string;
 successMessage: string;
}
const initialState: authState = {
 isSignedIn: false,
 currentUser: { uid: '', displayName: '' },
 isLoading: false,
 signUpError: '',
 signInError: '',
 successMessage: '',
};

// ------- AUTH ACTIONS --------------------------------
export const signInWithGoogle = createAsyncThunk(
    'user/signInWithGoogle',
    async () => {
        await signInWithGooglePopup();
    }
);
export const signInWithEmailAndPassword = createAsyncThunk(
    'user/signInEmailAndPassword',
    async (formFields: LoginFields) => {
        const { user } = await signInEmailAndPassword(
            formFields.email,
            formFields.password
        );
        return JSON.stringify(user);
    }
);
export const signUpUserEmailAndPassword = createAsyncThunk(
    'user/signUpUserEmailAndPassword',
    async (formFields: SignUpFields) => {
        const user = await signUpEmailAndPassword(formFields);
        return JSON.stringify(user);
    }
);
export const signoutUser = createAsyncThunk('user/signoutUser', async () => {
    return await logoutUser();
});

Enter fullscreen mode Exit fullscreen mode

signInWithGoogle() - calls sign in with google func

signInWithEmailAndPassword() - take args from the frontend, I stringify the user data before returning it, as data needs to be serialized first.

signUpUserEmailAndPassword() - take args from frontend and pass on helper func, again I stringify the returning user.

signoutUser() - calls logout helper func

I will be calling these functions in the UI.

Feel free to look into more detail about the createAsyncThunk and how it works on docs.


Reducers

I handled the response states and set the state accordingly.
On the .fulfilled response state for signInWithEmailAndPassword and signUpUserEmailAndPassword I get the data back, parse it and set it to state.

const authSlice = createSlice({
    name: 'auth',
    initialState,
    reducers: {

        setSignedIn(state, action) {
            state.isSignedIn = action.payload.signedIn;
            state.currentUser = action.payload.currentUser;
        },
        setSignupError(state, action) {
            state.signUpError = action.payload;
        },
        resetError(state) {
            state.signInError = '';
        },
    },
    extraReducers: (builder) => {
        builder
            .addCase(signInWithGoogle.rejected, (_, action) => {
                console.log('something went wrong with google sign-in', action.error);
            })
            // ---------------------------------------- SIGN IN ACTIONS ---------------------------------
            .addCase(signInWithEmailAndPassword.pending, (state) => {
                state.isLoading = true;
            })
            .addCase(signInWithEmailAndPassword.fulfilled, (state, action) => {
                const { uid, displayName } = JSON.parse(action.payload);
                state.isLoading = false;
                state.currentUser = { uid, displayName };
            })
            .addCase(signInWithEmailAndPassword.rejected, (state) => {
                state.isLoading = false;
                state.signInError = 'User does not exist in the database';
            })
            // --------------------------------------- SIGN UP ACTIONS ---------------------------------
            .addCase(signUpUserEmailAndPassword.pending, (state) => {
                state.isLoading = true;
            })
            .addCase(signUpUserEmailAndPassword.fulfilled, (state, action) => {
                const { displayName, uid } = JSON.parse(action.payload);
                state.isLoading = false;
                state.currentUser = { uid, displayName };
            })
            .addCase(signUpUserEmailAndPassword.rejected, (state, { error }) => {
                state.isLoading = false;
                state.signUpError = error.code;
            })
            // --------------------------------------- SIGN OUT ACTIONS ---------------------------------
            .addCase(signoutUser.fulfilled, (state) => {
                state.isLoading = false;
                state.isSignedIn = false;
            });
    },
});


export const { resetError, setSignupError, setSignedIn } = authSlice.actions;
export default authSlice.reducer;

Enter fullscreen mode Exit fullscreen mode

That's all for the auth/redux portion of the project, stay tuned!

Top comments (0)