Getting started with Redux in React

Lot’s of Getting started posts at the moment, and probably a lot more to come. This one’s on using Redux with React and TypeScript.

Let’s create a sample application…

  • yarn create react-app {insert app name} –typescript
  • yarn add redux
  • yarn add react-redux
  • yarn add @types/react-redux
  • We can now delete the App.* files and the *.svg
  • Add folder components to src and also reducers
  • In src/components add Counter.tsx (we’re going to recreate the redux example component for in a tsx). Here’s the code
    import React, { Component } from 'react';
    
    interface CounterProps {
        value: number;
        onIncrement: () => void;
        onDecrement: () => void;
    }
    
    export default class Counter extends Component<CounterProps, {}> {
        constructor(props: CounterProps) {
            super(props);
            this.incrementAsync = this.incrementAsync.bind(this);
            this.incrementIfOdd = this.incrementIfOdd.bind(this);
        }
    
        incrementIfOdd() {
            if (this.props.value % 2 !== 0) {
              this.props.onIncrement()
            }
          }
        
        incrementAsync() {
          setTimeout(this.props.onIncrement, 1000)
        }
    
        render() {
            const { value, onIncrement, onDecrement } = this.props
            return (
              <p>
                Clicked: {value} times
                {' '}
                <button onClick={onIncrement}>
                  +
                </button>
                {' '}
                <button onClick={onDecrement}>
                  -
                </button>
                {' '}
                <button onClick={this.incrementIfOdd}>
                  Increment if odd
                </button>
                {' '}
                <button onClick={this.incrementAsync}>
                  Increment async
                </button>
              </p>
            );
        }
    }
    
  • Now in src/reducers add the file counterReducer.ts and put the following code into it
    export default (state: number = 0, action: any) => {
        switch (action.type) {
          case 'INCREMENT':
            return state + 1
          case 'DECREMENT':
            return state - 1
          default:
            return state
        }
    }
    
  • Finally, replace the contents of the index.tsx with the following
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import * as serviceWorker from './serviceWorker';
    import { Provider } from 'react-redux'
    import { createStore } from 'redux'
    import counter from './reducers';
    import Counter from './components/Counter';
    
    const store = createStore(counter);
    
    const render = () => {
        ReactDOM.render(
            <Provider store={store}>
                <Counter
                    value={store.getState() as number}
                    onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
                    onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
                />,
            </Provider>, 
            document.getElementById('root'));
    }
    
    render();
    store.subscribe(render);
    
    serviceWorker.unregister();
    
  • If everything is correct you should be able to execute yarn start and see the web page with +, – buttons etc. and clicking + or – will increment and decrement the displayed counter.

That’s a lot to take in, so what have we actually done?

Redux supplied a storage container for our application state. In the index.tsx file we create a store passing our reducer (src/reducers/counterReducer.ts) Reducers are called in response to the store.dispatch calls. In this case within index.tsx we dispatch the actions with the type set to strings INCREMENT and DECREMENT. The reducer receives these messages then makes state changes (although not actually changing the state directly but returning a new state).

As the Redux documentation on reducers states, “Remember that actions only describe what happened, but don’t describe how the application’s state changes”.

The next piece of code to look at is

store.subscribe(render);

We’ve wrapped the render code for the application in the render function. Subscribe then allows us to listen to messages on the redux store and calls the render function to render changes. The following part of the Counter code within the index.tsx file then simply assigns the current store state to the Counter value property

value={store.getState() as number

There’s not lot to really say about the Counter component which is standard React code for displaying buttons etc. and exposing properties as used in index.tsx.

More…

This is a simplistic example. Redux documentation states that it’s not advisable to write the store.subscribe code but instead use the connect function provided by React Redux for this functionality.

In this instance we create a src/containers folder and add the file CounterContainer.ts, here’s the code

import {  connect } from 'react-redux'
import Counter from './../components/Counter';

const mapStateToProps = (state: any) => {
    return {
        value: state as number
    }
}

const mapDispatchToProps = (dispatch: any) => {
    return {
        onIncrement: () => dispatch({ type: 'INCREMENT' }),
        onDecrement: () => dispatch({ type: 'DECREMENT' }),
    }
}

export const CounterLink = connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter);

export default CounterLink;

Here we write code to map state to properties and dispatch to properties (if we don’t handle either of these we simply supply a null in place of the function in the connect function.

As you can probably see, in mapStateToProps, the state is supplied and we simply apply it to the value property (which is on our Counter). In mapDispatchToProps we link the onIncrement and onDecrement to the dispatch functions.

Finally in index.tsx replace the Counter component with our newly created and exported CounterLink, i.e. here the new index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import counterReducer from './reducers/counterReducer';
import CounterLink from './containers/CounterContainer';

const store = createStore(counterReducer);

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

serviceWorker.unregister();