Modern redux boilerplate

Basic react-redux setup that should work for any app using redux modern approach to state management. This will be a step by step guide without going too deeply into detail since it's mostly adding boilerplate.

The stack is Typescript, React, Redux, that's it!

I created a basic authReducer to manage session state and async calls.

Here's a repo with a fully working example, it also includes async calls using thunks.

Store

      
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
// This doesn't need to be in the 'reducers'
// directory, you can place the slice file
// inside your component directory, etc
import { authReducer } from '../reducers';

export const store = configureStore({
  reducer: {
    auth: authReducer
  }
});

export type AppDispatch = typeof store.dispatch;
// Typescript utility type to infere the return type of a function,
// in this case we are doing type-inference of the store.getState method.
// If we keep adding reducers to our store, this will update types automatically :)
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action
>;
      
          

Hooks

Create a file called hooks.ts.

These hooks will allow you to dispatch redux actions and retrieve data from state (selector).

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

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
      
          

Creating a "slice"

The slice is the bread and butter of modern redux, it'll have the reducers, provide actions and asynchronous flows. We'll use createSlice helper from @reduxjs/toolkit for this.

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

// The global app state types
import { RootState } from './store';
// Dummy async call to authenticate an user plus types
import { Auth, Session, authenticate } from '../services';
// This is not needed, but it's nice to have action names
// in constants that can be used globally
import { auth as authConstants } from './constants';

// We declare the types for this specific reducer
export interface AuthReducerState {
  authenticating: boolean;
  email?: string;
  authData?: Session;
}

// Declare some initial state if required
const initialState: AuthReducerState = {
  authenticating: false,
  email: undefined,
  authData: undefined
}; 

// A basic thunk to handle the async flow
// from here we are just invoking a promise
// that returns (or not!) an user session
export const loginAsync = createAsyncThunk(
  authConstants.APP_AUTH_CREATE_SESSION,
  async (loginData: Auth) => {
    try {
      const response = await authenticate(loginData); 

      return response;
    } catch (e) {
      console.error('WHOOPS SOMETHING WRONG IN loginAsync!!!')
      console.error(e);
    }
  }
);

// The actual slice
export const authSlice = createSlice({
  name: 'auth',
  initialState,
  // If we pass an object to the reducers key, redux
  // automatically combines them using combineReducers 
  reducers: {
    requesting(state) {
      // Modern redux uses immer internally, so
      // it's fine to mutate data on this manner,
      // you always will get an immutable object in the end
      state.authenticating = true;
    },
    // These are just for demo purposes, not needed here since
    // we have a single case and it is an async one (create an user session)
    // and those are being handled in the extraReducers section.
    fulfilled(state) {
      state.authenticating = false;
    }
  },
  extraReducers: (builder) => {
    builder
      // Thunks is a promise, so we have all 3 states here being handled
      // appropriately
      .addCase(loginAsync.pending, (state) => {
        state.authenticating = true;
      })
      .addCase(loginAsync.rejected, (state) => {
        state.authenticating = false;
        state.authData = undefined;
      })
      .addCase(loginAsync.fulfilled, (state, action) => {
        state.authenticating = false;
        state.authData = action.payload;
      })

  }
});

// Selectors
export const selectSessionData = (state: RootState) => state.auth.authData;
export const selectAuthenticating = (state: RootState) => state.auth.authenticating;

// Export actions from slice
export const authActions = authSlice.actions;
// Export reducer from slice
export const authReducer = authSlice.reducer;
      
          

The Login component

      
import React, { FormEvent, SyntheticEvent, useCallback, useState } from 'react';

import { useAppSelector, useAppDispatch } from './hooks';

import { Auth } from './services';

import { selectSessionData, selectAuthenticating, loginAsync } from './slices';

import './Login.css';

export const Login = () => { 
  const dispatch = useAppDispatch();
  const authenticating = useAppSelector(selectAuthenticating);

  const [auth, setAuth] = useState({
    email: '',
    password: ''
  });

  // Very basic validation just to make sure the
  // user submitted data through the form and 
  // track the pending to fulfilled/rejected states.
  //
  // Web form validation should be a lot stronger,
  // at least email syntax and password strength.
  const disableSubmit = (authData: Auth | undefined) => !authData || !authData.email || !authData.password || authenticating;

  const submit = useCallback((e: SyntheticEvent) => {
    e.preventDefault();
    dispatch(loginAsync(auth));
  }, [auth, dispatch]);

  const updateEmail = useCallback((e: FormEvent) => {
    setAuth({
      ...auth,
      email: e.currentTarget.value
    });
  }, [auth, setAuth]);

  const updatePassword = useCallback((e: FormEvent) => {
    setAuth({
      ...auth,
      password: e.currentTarget.value
    });
  }, [auth, setAuth]);


  return (
    <form className="Login" onSubmit={submit}>
      <label htmlFor="email">
        Email
      </label>
      <input id="email" type="text" onChange={updateEmail}/>
      <div className="separator hor" />
      <label htmlFor="email">
        Password
      </label>
      <input id="password" type="password" onChange={updatePassword} />
      <div className="separator hor" />
      <button disabled={disableSubmit(auth)} type="submit">Login</button>
    </form>
  );
};