Redux InitialState with TypeScript

Redux with TypeScript: Focus on InitialState

John Boardman JavaScript, TypeScript 2 Comments

In this post, I’m going to focus on Redux’s InitialState using TypeScript using the example project I’ve used for the last several blogs, Whirlpool. You can find my last post on the Keyhole Dev Blog – Updating Microservices with Netty, Kafka, and React: Whirlpool revisited. Feel free to go back and read about microservices, Netty, Kafka, and React, or just start here with me and continue on the journey. Either way, I’m glad you’re here.

As a reminder, you can find the code on my GitHub, here. The README.md file in the project will help you get going, along with the scripts provided for Mac, Linux, and Windows WSL.

Getting Set Up

After updating to the latest version of Kafka and making a small fix to the Java stock service due to changes in Yahoo’s finance page, I updated the React client to use TypeScript instead of JavaScript and Redux instead of React’s built-in Context.

The focus of this blog will be creating Redux’s InitialState using TypeScript. It tends to be tricky to get it to stop complaining about types, so this should be helpful. Personally, I’ve encountered this issue several times across multiple projects, so I think it is worth talking about.

Background

Redux is a package that helps store data in a common place, update that data, retrieve that data, and get notified when that data changes. One great reason to use Redux is the Redux DevTools Chrome extension, which gives you a wealth of information about the internal data, action calls, charts showing the hierarchy of your data, and other tools.

One of the ways to make Redux efficient is to break up the store into sections. This is generally where TypeScript gets tricky because if everything isn’t exactly perfect, it will complain. It can be tedious to properly fix this, but it complains for a good reason.

Everything it points out can lead to very difficult-to-find runtime errors or crashes, which of course, is why you want to be using a strongly typed language in the first place.

Meat and Potatoes

At the heart of configuring Redux is the createStore function. Well, it’s really the legacy_createStore function. There’s a new way to use Redux through a package built on top called Redux Toolkit (RTK), but I haven’t gone there quite yet.

All of the projects I work on still use the old way, and it works. It’s a bit more setup, but that’s ok. Anyway, here’s my src/store/configureStore.ts file in its entirety.

import {
  legacy_createStore as createStore,
  applyMiddleware,
  Store,
} from 'redux';
import { composeWithDevTools } from '@redux-devtools/extension';

// Thunk middleware allows actions to be chained and waited on by returning
// a function from that action
// https://github.com/gaearon/redux-thunk
import thunk from 'redux-thunk';

// Reducers
import InitialState from '../types/InitialState';
import rootReducer from '../reducers/rootReducer';

export default function configureStore(initialState: InitialState): Store {
  const store = createStore(
    rootReducer,
    initialState,
    composeWithDevTools(applyMiddleware(thunk))
  );

  return store;
}

This mostly looks like JavaScript except for the parameter initialState. The InitialState type and the rootReducer go hand in hand to make this work as we will see momentarily. Next, let’s look at how the InitialState type is defined in src/types/InitialState.ts.

import AppInterface from './AppInterface';
import StockInterface from './StockInterface';
import UpDownInterface from './UpDownInterface';
import WeatherInterface from './WeatherInterface';

declare interface InitialState {
  app: AppInterface;
  stock: StockInterface;
  upDown: UpDownInterface;
  weather: WeatherInterface;
}

export default InitialState;

Here’s where the magic begins. Remember when I said Redux is more efficient if the store is broken into sections (the Redux team calls these slices)? Here is where the sections are defined.

Grouping the data in this manner also makes it more logical and easier to consume as a developer. If I am only interested in the app fields, why muddy things up by having to see the stock, upDown, and weather fields? Each section gets its own reducer.

So, let’s look at the src/reducers/rootReducer.ts to see how it pairs with InitialState.

import { combineReducers } from 'redux';
import InitialState from '../types/InitialState';
import * as appTypes from '../modules/app/actions/types';
import app from '../modules/app/reducers';
import stock from '../modules/stock/reducers';
import upDown from '../modules/upDown/reducers';
import weather from '../modules/weather/reducers';

const appReducer = combineReducers({
  app,
  stock,
  upDown,
  weather,
});

const rootReducer = (
  state: InitialState | undefined,
  action: any
): InitialState => {
  if (action.type === appTypes.USER_LOGGED_OUT) {
    return appReducer(undefined, action);
  }
  return appReducer(state, action);
};

export default rootReducer;

Here, we pull in the reducer for each section and then combine them using Redux’s combineReducers function, which itself, returns a function for setting up the state.

We then create the rootReducer function, which either provides the state or undefined to reset the state when the user logs out. It can do this because this function is called for each action called on any reducer, so it acts as a runtime router to all of the section reducers.

The next piece of code to look at is src/reducers/initialState.ts. This is where the fields come from to populate the store with defaults.

import InitialState from '../types/InitialState';
import UpDownData from '../types/UpDownData';
import StockData from '../types/StockData';
import WeatherData from '../types/WeatherData';

const initialState: InitialState = {
  app: {
    isLoggedIn: false,
    websocket: null as any,
    clientName: '',
    loginHandler: undefined,
    logoutHandler: undefined,
  },
  stock: {
    removeStockHandler: undefined,
    stockList: [] as StockData[],
  },
  upDown: {
    removeUpDownHandler: undefined,
    upDownList: [] as UpDownData[],
  },
  weather: {
    removeWeatherHandler: undefined,
    weatherList: [] as WeatherData[],
  },
};

export default initialState;

This object instantiates the interfaces defined for the store. The key to why TypeScript is useful here is that if you forget to set any field on any section, TypeScript will flag it as an error. It lets you know at compile time.

With JavaScript, you would not know that you missed a field until it crashed at runtime because it couldn’t find it. The tricky part is splitting up the interfaces to be one per section and then combining them all into the InitialState interface.

Each section reducer uses the interface for its section. Let’s look at one of the reducers to see how that works. I’m going to choose the stock reducer in src/modules/stock/reducers/index.ts since it is smaller than others.

/* eslint-disable default-param-last */
import { AnyAction } from 'redux';
import * as types from '../actions/types';
import initialState from '../../../reducers/initialState';
import StockInterface from '../../../types/StockInterface';

export default (
  state: StockInterface = initialState.stock,
  action: AnyAction
): StockInterface => {
  switch (action.type) {
    case types.SET_REMOVE_STOCK_HANDLER:
      return {
        ...state,
        removeStockHandler: action.removeStockHandler,
      };
    case types.SET_STOCK_LIST:
      return {
        ...state,
        stockList: action.stockList,
      };
    default:
      return state;
  }
};

The reducer for a section only looks at its piece of the entire state, which is why state is defined to be initialState.stock. It is also why StockInterface is the type.

When coding each case in the reducer, since the type is set, the state fields such as stockList are both name-checked (so you can’t type stickList as that is not a field in StockInterface) and type-checked (making sure you don’t set a boolean where a string is expected. Finally, this reducer only updates its section instead of having to update the entire state each time the reducer is invoked, which is where efficiency comes into play.

Conclusion

I hope it is plain to see how using TypeScript and breaking up state help ensure you have a stable and efficient application. I’ve used this pattern multiple times and plan to continue.

I’m glad the days of JavaScript are coming to an end. Coding in it now seems like flying by the seat of your pants – like writing code and hoping for the best.

With TypeScript, on the other hand, you know you are doing your best because it’s always looking out for you. I can’t say I’m an expert in TypeScript yet, but what I’ve learned so far has definitely helped me produce better code than I have in the past with plain JavaScript.

Thanks for stopping by!

0 0 votes
Article Rating
Subscribe
Notify of
guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments