React-Redux Hooks with Typescript in 2020

This is going to be a continued version of our previous blog, React Hooks with Typescript, So if you are new to hooks, I would suggest looking into that article first, which talks about setting up the starter kit of react hooks with typescript and AntD for UI components.

If you know basic of react hooks, like useEffect or useState, you can even give it a skip.

Since React push using of functional components, a lot of libraries around react start publishing their own hooks, Redux being one of them, as almost most react project use redux these days.

The Redux hooks APIs provide an alternative to connect HOC and go away with mapStateToProps, and mapDispatchToProps, and I must say these hooks API provide a much cleaner way of doing that.

Now without going much in theory, lets deep dive into the coding part.

create a new component file src/components/ReduxHooksComponent.tsx, if not already with 2 input fields and a submit button

Create a Normal Functional component


import React, {ChangeEvent, FormEvent, useState, useEffect} from "react";
import {Form, Input, Button} from "antd";

interface Props {
}

const ReduxHooksComponent: React.FC<Props> = () => {


    return (
        <Form layout="inline">
            <Form.Item>
                <Input type="text" placeholder="name"/>
                <Input type="text" placeholder="address" />
                <Button htmlType="submit" type="primary"> Submit </Button>
            </Form.Item>
        </Form>
    )
};

export default ReduxHooksComponent;

Now import this component in App.tsx


import React from 'react';
import './App.css';
import ReduxHooksComponent from "./components/ReduxHooksComponent";

const App: React.FC = () => {
  return (
    <div className="App">
      <ReduxHooksComponent/>
    </div>
  );
};

export default App;

Pretty Simple. Right! after running the code it should render a component with 2 input and a submit button.

Setting up the store, actions, and reducers.

Firstly add redux and react-redux to the project


yarn add react-redux @types/react-redux redux

Create two files, src/store/index.ts and src/store/root-reducer.ts

let’s start creating each and every component of root reducer, which would be actions, states, reducers

# src/store/root-reducer.ts

import {Action, Reducer} from "redux";

export interface InitialState {
    name: string;
    address: string;
}

export const initialState: InitialState = {
    name: '',
    address: '',
};

export interface DispatchAction extends Action {
    payload: Partial<InitialState>;
}

export const rootReducer: Reducer<InitialState, DispatchAction> = (state, action) => {
    return initialState;
};


Now we have a simple reducer that does nothing but returns the initial state.

Let’s create a store using this rootReducer, so our src/store/index.ts will look like


import {DispatchAction, InitialState, rootReducer} from "./root-reducer";
import {createStore} from "redux";


export const store = createStore<InitialState, DispatchAction, null, null>(rootReducer);

Also, updating the index.tsx file to wrap App with Provider ans provide the store to the provider.

# src/index.tsx

........
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
........

That’s all, well you won’t see any visible changes on the browser, but eventually, you have integrated a redux into your react code.

Now let’s create some actions. Update the root-reducer.ts so that it should look something like

# src/store/root-reducer.ts

.......
export interface DispatchAction extends Action<ActionType> {
    payload: Partial<InitialState>;
}

export enum ActionType {
    UpdateName,
    UpdateAddress,
    DeleteName,
    DeleteAddress,
}

export const rootReducer: Reducer<InitialState, DispatchAction> = (state = initialState, action) => {
    if (action.type === ActionType.UpdateName) {
        return {...state, name: action.payload.name || ''};
    } else if (action.type === ActionType.DeleteName) {
        return {...state, name: ''};
    } else if (action.type === ActionType.DeleteAddress) {
        return {...state, address: ''};
    } else if (action.type === ActionType.UpdateAddress) {
        return {...state, name: action.payload.name || ''};
    } else return state;
};

Pretty Simple, Yes! We have just returned an updated version of the state with new values as per the actions suggest.

Lets create a Dispatcher too in the same file

src/store/root-redux.ts

.......
export class RootDispatcher {
    
    private readonly dispatch: Dispatch<DispatchAction>;
    
    constructor(dispatch: Dispatch<DispatchAction>){
        this.dispatch = dispatch; 
    }
    updateName = (name: string) => this.dispatch({type: ActionType.UpdateName, payload: {name}});
    
    updateAddress = (address: string) => this.dispatch({type: ActionType.UpdateAddress, payload: {address}});
    
    deleteName = () => this.dispatch({type: ActionType.DeleteName, payload: {}});
    
    deleteAddress = () => this.dispatch({type: ActionType.DeleteAddress, payload: {}})
}
......

Well, now we are done with all the dispatchers, actions, store. basically our entire redux is setup. Now all we need to do is dispatch actions from components and use values from the store.

Using Dispatcher and Store in Components via Hooks

Redux hooks provide 2 main hooks, useSelector and useDispatch

useSelector give access to the entire store object, and we can select only what we are interested in

useDispatch give access to dispatch, which will be used to create the Dispatcher object of RootDispatcher and then dispatch events to update state.

Let’s create an interface of all the property we are interested in accessing from the store,


interface StateProps {
    name: string;
    address: string;
}

UseSelector to assign name and address state


    const {name, address} = useSelector<InitialState, StateProps>((state: InitialState) => {
        return {
            name: state.name,
            address: state.address
        }
    });

Now useDispatch to get a dispatch object and create a new instance of RootDispatcher


const dispatch = useDispatch();
const rootDispatcher = new RootDispatcher(dispatch);

After integrating both state and dispatcher from to the component our file will look something like

#src/components/ReduxHooksComponent.tsx

import React, {ChangeEvent, FormEvent, useState, useEffect} from "react";
import {Form, Input, Button} from "antd";
import {useDispatch, useSelector} from "react-redux";
import {InitialState, RootDispatcher} from "../store/root-reducer";

interface Props {
}

interface StateProps {
    name: string;
    address: string;
}

const ReduxHooksComponent: React.FC<Props> = () => {

    const {name, address} = useSelector<InitialState, StateProps>((state: InitialState) => {
        return {
            name: state.name,
            address: state.address
        }
    });

    const dispatch = useDispatch();
    const rootDispatcher = new RootDispatcher(dispatch);


    return (
        <Form layout="inline">
            <Form.Item>
                <Input type="text" placeholder="name" value={name}
                       onChange={(e: ChangeEvent<HTMLInputElement>) => {
                           rootDispatcher.updateName(e.target.value)}
                       }
                />
                <Input type="text" placeholder="address" value={address}
                       onChange={(e: ChangeEvent<HTMLInputElement>) =>{
                           rootDispatcher.updateAddress(e.target.value)}
                       }
                />
                <Button htmlType="submit" type="primary"> Submit </Button>
            </Form.Item>
        </Form>
    )
};

export default ReduxHooksComponent;

NOTE: The selector is approximately equivalent to the mapStateToProps argument to connect conceptually. The selector will be called with the entire Redux store state as its only argument. The selector will be run whenever the function component renders. useSelector() will also subscribe to the Redux store, and run your selector whenever an action is dispatched.

useSelector() uses strict === reference equality checks by default, not shallow equality, to use shallow Equality, we can use shallowEqual from react-redux

so our selector code would look something like


    const {name, address} = useSelector<InitialState, StateProps>((state: InitialState) => {
        return {
            name: state.name,
            address: state.address
        }
    }, shallowEqual);

Can I use hooks with redux?

Yes, React-Redux released its latest version 7.1 which includes useSelector and useDispatch using which you can create React-Redux app with less and more readable code.

What is useSelector hook?

useSelector, allow us to extract data from the redux store. It is equivalent to mapStateToProps, as we use in class-based component. It has two arguments one is “selector” function and the other is “equalityFn” function, which is optional.

What is useDispatch hook?

useDispatch returns a reference of dispatch function from redux store using this reference we can dispatch our actions to the reducer. dispatch function takes an object with two properties “Type” and “Payload”.

What is the second argument of useSelector hook?

The second argument is a “shallowEqual” function (imported from “react-redux”) which compares the previous result value with the current result value of the selector and the comparison is shallow i.e “==”. If you don’t pass it then it use the default strict comparison “===”.

2 thoughts on “React-Redux Hooks with Typescript in 2020”

  1. very nice, but why create a rootDispatcher Class? now it’s initiated every time `ReduxHooksComponent` rerenders. Won’t it be cleaner to create an action function that returns an object containing the keys `type` and `payload`?

    1. Probably we can use a Singleton method to instantiate the object, or converting all the dispatch actions to static method and passing dispatch as parameters to each actions. Or probably we can even create the new object under useEffect.

Leave a Comment

Your email address will not be published. Required fields are marked *