DEV Community

loading...
Cover image for Typescript, React, Redux, Thunk, and Material-ui template -- now with less boilerplate!

Typescript, React, Redux, Thunk, and Material-ui template -- now with less boilerplate!

thatonejakeb profile image Jacob Baker ・3 min read

tldr : I've forked the official Redux Typescript create react app template, switched it to use functional components and added Material-UI support. Source code is here, follow me on Twitter for updates and nonsense, stay safe and wash your hands ❤️


Last week I wrote about the updated version of my Typescript, React, Redux, Thunk, and Material-UI skeleton app.

Well, as is the case with most tech stuff, it has been superseded by a new and improved version!

In the comments of the previous post I was kindly pointed towards the official Redux+Typescript CRA template by a maintainer which, amongst other things, uses Redux Toolkit to reduce the amount of boilerplate needed.

Just as a brief example of just how much is cut out, in the original skeleton you have the following breakdown of files for each feature:

  • actions.ts
  • types.ts
  • reducer.ts
  • thunk.ts

Whereas using Redux Toolkit you end up with:

  • slice.ts

Equally nice is there is no more need for connect or mapStateToProps and the confusion they bring.

So, for a concrete example, here's how I had the counter implemented without Redux Toolkit:

types.ts

export interface ExampleState {
    isFetching: boolean;
    count: number;
    error?: {
        message: string
    }
}

export const FETCH_EXAMPLE_REQUEST = "example/FETCH_EXAMPLE_REQUEST";
export const FETCH_EXAMPLE_SUCCESS = "example/FETCH_EXAMPLE_SUCCESS";
export const FETCH_EXAMPLE_FAILURE = "example/FETCH_EXAMPLE_FAILURE";

interface FetchExampleRequestAction {
    type: typeof FETCH_EXAMPLE_REQUEST,
    payload: {
        isFetching: boolean
        error: {
            message: string
        }
    }
}

interface FetchExampleSuccessAction {
    type: typeof FETCH_EXAMPLE_SUCCESS,
    payload: {
        isFetching: boolean
        count: number
    }
}

interface FetchExampleFailureAction {
    type: typeof FETCH_EXAMPLE_FAILURE,
    payload: {
        isFetching: boolean,
        error: {
            message: string
        }
    }
}

export type ExampleActionTypes = FetchExampleRequestAction | FetchExampleSuccessAction | FetchExampleFailureAction;

action.ts

import { FETCH_EXAMPLE_REQUEST, FETCH_EXAMPLE_SUCCESS, FETCH_EXAMPLE_FAILURE } from "./types";

export function fetchExampleRequest() {
    return {
        type: FETCH_EXAMPLE_REQUEST,
        payload: {
            isFetching: true,
            error: undefined
        }
    }
}

export function fetchExampleSuccess(count: number) {
    return {
        type: FETCH_EXAMPLE_SUCCESS,
        payload: {
            isFetching: false,
            count
        }
    }
}

export function fetchExampleFailure(message: string) {
    return {
        type: FETCH_EXAMPLE_FAILURE,
        payload: {
            isFetching: false,
            error: {
                message
            }
        }
    }
}

reducer.ts

import {
    ExampleState,
    FETCH_EXAMPLE_REQUEST,
    FETCH_EXAMPLE_SUCCESS,
    FETCH_EXAMPLE_FAILURE,
    ExampleActionTypes
} from "./types";

const initialState: ExampleState = {
    isFetching: false,
    count: 0,
    error: undefined
};

export function exampleReducer(
    state = initialState,
    action: ExampleActionTypes
): ExampleState {
    switch(action.type) {
        case FETCH_EXAMPLE_REQUEST:
            return {
                ...state,
                isFetching: action.payload.isFetching,
                error: action.payload.error
            };
        case FETCH_EXAMPLE_SUCCESS:
            return {
                ...state,
                isFetching: action.payload.isFetching,
                count: action.payload.count
            }
        case FETCH_EXAMPLE_FAILURE:
            return {
                ...state,
                isFetching: action.payload.isFetching,
                error: action.payload.error,
            }
        default:
            return state;
    }
}

thunk.ts

import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import {
  fetchExampleRequest,
  fetchExampleSuccess,
  fetchExampleFailure
} from "./actions";
import { AppState } from "../";

export const fetchExample = (
  count: number
): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
  dispatch(fetchExampleRequest());

  setTimeout(() => {
    var randomErrorNum = Math.floor(Math.random() * count) + 1;

    if (randomErrorNum === count) {
      dispatch(fetchExampleFailure("Unable to increment count."));
    } else {
      dispatch(fetchExampleSuccess(count + 10));
    }
  }, 1000);
};

Which I might add is an incredibly small, novel, feature and that level of boilerplate is needed for each feature added. It can get quite irritating.

Now on to the new implementation that takes advantage of Redux Toolkit.

slice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, RootState } from '../../app/store';

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const slice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.value += 10;
    },
  },
});

export const { increment, incrementAsync } = slice.actions;

export const incrementAsync = (): AppThunk => dispatch => {
  setTimeout(() => {
    dispatch(increment());
  }, 1000);
};

export const selectCount = (state: RootState) => state.counter.value;

export default slice.reducer;

And that's it for definitions!

You would be right in asking where the isFetching part went. In this example I've replaced it with a State Hook in the component:

const [isWorking, setIsWorking] = useState(false);

// [.. snip...]

const doAsyncIncrement = () => {
  setIsWorking(true);
  dispatch(incrementAsync(Number(incrementAmount || 0)));

  setTimeout(() => {
    setIsWorking(false);
  }, 1000);
};

// [... snip...]

<Button 
  className={classes.button}
  onClick={() =>
    doAsyncIncrement();
  }
>
Increment
</Button>

It such a nice way of working that I've forked the CRA Redux Template and ported my original skeleton app. I've added Material-UI support and switched it to use functional components.

You can find the source here:

Github: https://github.com/jacobbaker/cra-template-redux-typescript-mui

Or start a new project using it with:

npx create-react-app my-app --template redux-typescript-mui

Let me know if you've any comments or questions here or @thatonejakeb.

Discussion (0)

pic
Editor guide