0

From Novice to Pro: Master Redux with Typescript - Level 3

Master how Redux can be used as a singleton state manager instead of Context API, how redux-toolkit reduces boilerplate code.

article cover image

TurjoyFebruary 25, 2024

Goals

In this article we will explore how redux can be used as a global state manager.

Here's the full app from outside....

Background Knowledge

This article skips concepts like basic react native setup and react navigation.

To understand those you can explore past articles:

React Native basic setup and pre-steps

Screen planning and React Navigation

What is Redux?

Redux is a global state management library.

It can be used with any Javascript library or framework.

It has five basic parts:

  • Store
  • Action
  • Reducer
  • Selector
  • Dispatch

Store

Redux Store is the central place where all global data is stored.

Since we deal with state of the data, we say global state is stored.

It is a single source of truth for the application.

Actions

An action is a plain JavaScript object that has a type field.

Action describes an event that's about to happen.

It can pass data to the event too as 'payload'.

It mentions the event using 'type'.

Like...

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Reducers

A reducer is a function that receives the current state and an action object.

Based on those it decides how to update the state if necessary.

Then it returns the new state.


(state, action) => newState

So reducer is an event listener which handles events based on the received action (event) type.

Selector

For a component to have access to the data it needs from store, selectors are used.

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)

Dispatch

To update the store, redux provides store.dispatch().

It gets the 'type' and 'payload' from actions.

Then it tells the reducer make updates based on those.

none provided

React Redux

We can get the current data from the store, using store.getState().

But what happens when the state updates in real time?

Can the UI detect the change and update itself?

No.

So we need react-redux to let UI know whenever there's a change in the store state.

It lets us skip writing a lot of Observer Pattern boilerplate code.

Resource about Observer Pattern is provided in resources to read later.

Why use Redux?

Sometimes handling multiple states from multiple components efficiently, can become challenging when the application grows in size.

Redux comes to save us in this scenario.

It takes responsibility for all those kind of data.

Thus,

  • improving code consistency
  • providing better testable codes
  • acting as a single source of truth

How to use Redux like a Pro

Here's the code that we will be using as reference.

After you are done understanding, you can implement yourself in this code.

Just replace context API with Redux-toolkit.

Using redux-toolkit

Redux asks a lot of boilerplate code.

The dispatch needs those to determine the reducer function.

The redux-toolkit provides special hooks that makes the boilerplate code unnecessary.

Like if you use plain redux, you need to define actions, reducers separately.

You would have to define in actions what the reducer must do after dispatch.

But with redux-toolkit you define reducer functions using 'createSlice()' hook.

Each reducer functions will have their respective actions generated.

Sometimes the action involves some logic before reducer does its job.

Like if the action handles API call.

We can define an action and add to the reducer.

This will be covered in the next article when we will learn to use REST APIs.

Folder structure

In the src directory (or root) we create a 'store' folder.

Then create the following structure of files and folders.

redux-typescript-store-folder-structure.webp

Defining reducers

We provide an initial state of the data we need.

Then we define the types of reducer functions we want on a data.


import {PayloadAction, createSlice} from '@reduxjs/toolkit';
import {Task} from '../../../types';

interface todoSliceState {
  data: Task[];
}

// Define the initial state using that type
const initialState: todoSliceState = {
  data: [],
};

interface updateProps {
  index: number;
  description: string;
}

export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  reducers: {
    addTodo: (state, action: PayloadAction<string>) => {
      state.data = [
        ...state.data,
        {pending: true, description: action.payload},
      ];
    },
    updateTodoDetails: (state, action: PayloadAction<updateProps>) => {
      const updatedTasks = [...state.data];
      updatedTasks[action.payload.index].description =
        action.payload.description;
      state.data = updatedTasks;
    },
    updateTodoStatus: (state, action: PayloadAction<number>) => {
      const updatedTasks = [...state.data];
      updatedTasks[action.payload].pending =
        !updatedTasks[action.payload].pending;
      state.data = updatedTasks;
    },
    deleteTodo: (state, action: PayloadAction<number>) => {
      const updatedTasks = [...state.data];
      updatedTasks.splice(action.payload, 1);
      state.data = updatedTasks;
    },
  },
});

// Other code such as selectors can use the imported `RootState` type

export default todoSlice.reducer;


Getting action functions

We get the action functions from the created slice.


import {todoSlice} from '../reducers/todo';

export const {addTodo, updateTodoStatus, updateTodoDetails, deleteTodo} =
  todoSlice.actions;

Configuring the store

Then with the reducers in place, we configure the store.

Since we are using Typescript, we also get the types we will be using.

RootState and AppDispatch.


import {configureStore} from '@reduxjs/toolkit';
import todo from './reducers/todo';

const store = configureStore({
  reducer: {
    todo,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

Defining the hooks

We need two hooks.

One to get data (lets name it useAppSelector).

And other to dispatch actions to invoke reducers (lets name it useAppDispatch).


import {useDispatch, useSelector} from 'react-redux';
import type {TypedUseSelectorHook} from 'react-redux';

import type {RootState, AppDispatch} from '..';

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

Selectors

We also define selector to reuse code in different parts of the App.


import {useAppSelector} from '../hooks/redux-hooks';

const useTodoData = () => {
  const todos = useAppSelector(state => state.todo.data);
  return todos;
};

export default useTodoData;

We would have to repeat this logic every time we want to get data from store.

Instead we create a selector function that provides us with the updated store values.

Provider

Finally we encapsulate the <App/> with Provider form react-redux.


import React from 'react';
import {PaperProvider} from 'react-native-paper';
import AppNavigator from './src/navigation/AppNavigator';

import {Provider} from 'react-redux';

import store from './src/store';

function App(): React.JSX.Element {
  return (
    <PaperProvider>
      <Provider store={store}>
        <AppNavigator />
      </Provider>
    </PaperProvider>
  );
}

Invoke current data

We simply call the useTodoData() to get current state.


const tasks = useTodoData();

Update state

We invoke the useDispatch() function and pass the action.



const CustomCheckBox = ({item, index}: {item: Task; index: number}) => {
  const dispatch = useAppDispatch();

  return (
    <Checkbox.Android
      status={!item.pending ? 'checked' : 'unchecked'}
      onPress={() => dispatch(updateTodoStatus(index))}
      color={Colors.text}
    />
  );
};

Design concepts exploited

Redux - Singleton

The Singleton pattern is a creational design pattern that states:

  • a class has only one instance
  • provides a global point of access to that instance.

Redux is based on it.

Redux follows the principles of a single immutable state tree.

Redux provides global access to itself.

React-redux - Observer

The Observer Pattern defines a one-to-many dependency between objects.

When one object changes state, all of its dependents are notified.

React-Redux is a UI binding library based on that.

Last Words

Sometimes using redux is a overkill.

Like we were happy with just local storage if there was one screen.

It's also unnecessary if multiple components don't need the same data or functionality.

I have showed here a simplified reducer with everything required for scalability.

Use it to make complex ones with multiple reducers.

Keep me posted of your progress.

Resources

Code

Redux

Redux-toolkit

React-redux

Singleton Pattern

Observer Pattern

Design Patterns in Javascripts

Liked what you read?

Join my email list to get more...