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).
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;
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;
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>
);
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;
};
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();
});
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;
That's all for the auth/redux portion of the project, stay tuned!
Top comments (0)