React + TypeScript + Redux Toolkit - Safety and Simplicity

In this article I'll explain how Redux Toolkit simplifies building Redux based apps and combines beautifully with React and TypeScript for an all-round great stack.

Why am I talking about this?

I wanted to evangelise a stack that we've enjoyed and had great success with here at Instil - React + TypeScript + Redux Toolkit. But I wasn't sure of what aspects to talk about or who to pitch it to. Looking at The State of JavaScript 2019, I know a lot of you are building applications with React,

Front End Frameworks

Front End Frameworks Front End Frameworks

I also know, that in terms of languages, TypeScript is very popular (although not necessarily with React),

JavaScript Flavours JavaScript Flavours

Looking at managing data, Redux is also popular but an interesting figure below is the 19.8% who've used Redux before, but would not use it again.

Data Layer Data Layer

Also, looking at Google Trends, the number of searches for Redux dwarf those of Redux Toolkit.

Trends

So, in this article I'm focusing on,

  • The benefits of decoupling state from view generally
  • A quick review of how Redux does this
  • Using Redux Toolkit to simplify Redux
    • Especially important if you've tried plain Redux and found it difficult
  • All presented in the context of React and TypeScript

My hope is that by the end of this article you'll find Redux less intimidating and you'll be encouraged to try it along with TypeScript for a React project. The sample app I've created for the article is a basic web-based calculator,

Calculator

I'm assuming you have basic working knowledge of React and TypeScript (although perhaps not together or not in anger). If you'd like deeper dives into anything I cover, please Like and leave a comment below.

Decoupling State and UI

React is a framework entirely focused on building views, and it does that very well. The succinct syntax and interleaving of JSX view descriptors with standard programming elements facilitates easy construction of a component-based UI.

export const Buttons = () =>
  <div>
    {buttons.map((row, rowIndex) => (
       <div key={rowIndex} className='row no-gutters'>
         {row.map((button, colIndex) => <Button key={colIndex} value={button}/>)}
       </div>
     ))}
  </div>

What it doesn't do so well is handle state. Coupling state to our views and passing state through large trees of components is fragile and clumsy. Whenever state is owned by components, more business logic will reside in them which compounds the issue.

A much more elegant solution to separate our state & domain logic from our views. Then we can arbitrarily choose which components to bind to - we only bind what is needed, where it is needed.

Decoupling View and State

Decoupled, the state and domain logic are easier to develop. Removing view concerns makes debugging and testing much easier and allows us to focus on simple data transformations. We can model our system as data and descriptions of how our data changes based on events entering the system. This allows us to greatly reduce the cognitive load as we build more complex applications.

The view is also easier to work with as components are smaller and focused on simply rendering a template based on some state. It becomes easy to restructure the UI, splitting or coalescing components. We can move and share where state is utilised within the tree easily as we can arbitrarily bind any state to any part of the view. For example, duplicate a basket total from a component at the bottom of the screen to a toolbar at the top or utilise a busy status across several components.

Redux Refresher

Redux is a common state management framework and is often paired with React (which also promotes the functional and immutable thinking that Redux favours).

In the Redux model we have,

  • Store – this contains and manages our state
  • Actions – describes a state change (you may think of this as an event)
    • A simple object describing the change
    • Action Creator functions make it simpler to generate them
  • Reducer – a pure function that takes the current state, an action and will compute the next state

So, the general flow is that we start with some initial state and then we can dispatch actions to the store which will use the reducer to compute the new state. The view listens to the store and is notified when something changes.

Redux

Without going into too much detail, the system relies on the immutability of the state data structure which is why the reducer is a pure function (no side effects). When we have immutable data structures, checking if something has changed (and therefore knowing if the UI must rerender) is trivial as it is simply a reference comparison - to be different, it must be a different object.

Calculator

Considering our Calculator app, how would we present this in Redux?

  • State - What will the system need to represent?
    • The current number being entered
    • The previous result
    • The current operation
  • Actions - What events can happen to our application?
    • Button pressed - these could be further decomposed e.g. number, operation, equals etc
  • Reducer - Process action + state to produce the next state
    • Handle a button press

Redux Toolkit

Implementing the actions, reducer and state in normal Redux requires writing lots of boilerplate code. The pure function and immutability constraint on the reducer also further complicates things.

In the past I've written my own helpers to alleviate some of this but the Redux Toolkit is now here to remove the need of doing this yourself. It is easily added to a project,

npm install --save @reduxjs/toolkit

What's great is it comes with TypeScript bindings included.

Actions, Reducers and Slices

Toolkit provides helpers for creating standard elements - store, reducer, action, async actions etc. Even more useful though is the concept of a Slice, which allows us to set up our state, reducer and actions in a single container.

const slice = createSlice({
  name: 'some-name',
  initialState: {
    // ...
  },
  reducers: {
    // ...
  }
});

The reducers object defines functions that both define an action and the reduction logic for that particular action. What's more, the reducer function can be written in the immutable style, returning a new state, or can be written in a mutable style. The toolkit uses another library, Immer, so that the function can be written using simple mutation. Behind the scenes Immer will pass a proxy state object, track changes and then perform the immutable transformations required.This can greatly simplify some operations like making changes in deeply nested object structures or arrays and other data structures.

The slice generates the pure reducer functions and a collection of action creator functions that can be exported. For example, with our calculator we end up with,

export interface State {
  value: string;
  operation?: Operation;
  previousValue?: string;
}

const slice = createSlice({
  name: 'calculator',
  initialState: {
    value: '0',
    previousValue: undefined,
    operation: undefined
  } as State,
  reducers: {
    keyPressed(state: State, {payload: key}: PayloadAction<string>) {
      // ...
    }
  }
});

export const reducer = slice.reducer;
export const {keyPressed} = slice.actions;

The great thing about all of this is it is type safe (and not with very much extra annotations). The action creators generated, such as keyPressed, will take the correct parameter types based on the function definition within the reducers section.

keyPressed(state: State, {payload: key}: PayloadAction<string>) {
  // ...
}
...
keyPressed('3'); // Valid
keyPressed(3);   // Invalid

We can also include actions created externally (either in another slice or using the create action helpers) within an extraReducers section of our createSlice params.

Store

Creating a store is also easier with the toolkit. The configureStore function creates a store using a reducer just like the old createStore function but it wires in useful middleware by default. Middleware is the way Redux stores can be extended to process actions in a centralised manner. By default you have none but with the toolkit it wires in:

  • Redux Thunk - allows us to submit functions as actions for asynchronous actions
  • Redux Dev Tools - enables the Redux Dev Tools browser extension
  • Immutability Invariant - ensures that state transitions are always immutable
  • Serializability Invariant - ensures action and state are always serializable

The bottom 3 are automatically only enabled in debug mode.

const store = configureStore({
	reducer
});

Testability

So far, I've been able to develop the core logic of the app without focusing very much on the view. This separation of concerns is very powerful. It also facilitates easy testing as the reducer function is pure and we simply need to specify inputs and outputs.

it(`should handle previous operation when new operation pressed`, () => {
  const result = target({
    value: '10',
    previousValue: "12",
    operation: '-'
  }, keyPressed('+'));

  expect(result.previousValue).toEqual(undefined);
  expect(result.value).toEqual('2');
  expect(result.operation).toEqual('+');
})

it(`should replace op when op present but no previous value`, () => {
  const result = target({
    value: '10',
    previousValue: undefined,
    operation: '-'
  }, keyPressed('+'));

  expect(result.previousValue).toEqual(undefined);
  expect(result.value).toEqual('10');
  expect(result.operation).toEqual('+');
});

Leveraging the fact that we define tests by calling the it (or test) function we can write parameterised tests easily by using a forEach call,

[
  {value: '0', key: '0', expected: '0', name: 'Enter 0 with zero already present'},
  {value: '0', key: '4', expected: '4', name: 'Enter 4 with zero already present'},
  {value: '',  key: '1', expected: '1', name: 'Enter 1'},
  {value: '1', key: '2', expected: '12', name: 'Second digit'},
  {value: '0', key: '.', expected: '0.', name: 'Entering . on zero'},
  // ...
].forEach(({value, key, expected, name}) =>
  it(`should handle key presses correctly - ${name}`, () => {
    // ...
  })
);

Again, all of this is nicer in TypeScript as the test data object can be any shape but the compiler still knows the type and will give us error-checking, auto-complete, type checking, rename refactoring etc.

Note, within Jest, it and test have parameterised forms built in, but they are not as nice for complex test data as using the forEach.

Connect to React

Now that we've covered building out our state and domain logic, let's dig into the view. The toolkit doesn't help with connecting the state to the react views, but the core react-redux library has itself evolved to support Redux Hooks. This makes it easy to connect any part of our state to components using the useSelector hook function.

export const Display = () => {
  const value = useSelector((state: State) => state.value);
  const operator = useSelector((state: State) => state.operation);

  return (
    <div className='row'>
      <div className='col-1'>{operator}</div>
      <div className={`col-5 border text-right ${styles.display}`}>
        {value}
      </div>
    </div>
  );
}

With the useSelector hook, not only are we using this state when we render, we're setting this component up so it will automatically re-render if this state changes. Note, it is not if any state changes, just the extracted state.

Likewise, it is easy to dispatch any action using the useDispatch hook

const Button: FC<ButtonProps> = ({value}) => {
  const dispatch = useDispatch();

  return <button onClick={() => dispatch(keyPressed(value.content))}>
           {value.content}
         </button>;
}

Notice as well that this component is strongly typed. React has great TypeScript support too and we can type our functional components with FC and add an optional props type, in this case ButtonProps,

interface ButtonProps {
  value: ButtonDescriptor;
}

TypeScript will ensure the props are correctly typed, easily destructured and that our components are used correctly within JSX.

Conclusion

I hope that this article gives you some insights into using React and Redux together in a modern setting, utilising TypeScript and the Redux Toolkit to increase safety and reduce boilerplate. There's lots more detail that I could go into so please Like the article and leave a comment below for other topics for future articles.

Checkout our React and TypeScript courses. We deliver virtually to companies all over the world and are happy to customise our courses to tailor to your team's level and specific needs. Come and check us out to see if we can help you and your team.

Article By
blog author

Eamonn Boyle

Instructor, developer