Understanding React and Redux with tests – 01

Starting simple

In my previous post I’ve introduced the concept of Redux; my take on it, at least.
I want to implement more dynamic behaviors in my Local addon and to accomplish that I feel like Redux might be the solution; what it should provide is a global state that any component used by the application will be able to read from and write to; I’ve used the “event bus” metaphor to try and wrap my mind around it.
Before I delve into the technicalities of the stack at hand, a Local addon, I would liket to understand how Redux works in a simpler scenario.
Since I am myself that “simpler scenario” will be a test; leaving the addon code alone I would like to discover how Redux works, and what it can do for me, in a progressive and test-driven manner.

Running a first test

To understand how Redux works step by step I’ve written a first simple test.
The objective of the test is to understand how to wire a basic Redux based application before the complications associated with multiple files management and asynchronous operations come into the picture.
Forgetting about React and components and classes this first test allows me to understand how a Redux store, a fundamental concept in the library, can be used.

const React = require( 'react' )
const createStore = require( 'redux' ).createStore
const expect = require( 'chai' ).expect

const TOGGLE_STATUS = 'TOGGLE_STATUS'

const toggleStatus = function () {
    return {
        type: TOGGLE_STATUS,
    }
}

const status = function ( state = 'inactive', action ) {
    switch ( action.type ) {
        case TOGGLE_STATUS:
            return state === 'inactive' ? 'active' : 'inactive'
        default:
            return state
    }
}

const buttonText = function ( state = 'Deactivate', action ) {
    switch ( action.type ) {
        case TOGGLE_STATUS:
            return state === 'Deactivate' ? 'Activate' : 'Deactivate'
        default:
            return state
    }
}

function reducer( state = {}, action ) {
    return {
        status: status( state.status, action ),
        buttonText: buttonText( state.buttonText, action ),
    }
}


const initialState = {
    status: 'inactive',
    buttonText: 'Activate',
}

let store = createStore( reducer, initialState )

describe( 'Basic Redux', function () {
    it( 'should correctly change the status store', function () {
        const store = createStore( reducer, initialState )

        expect( store.getState().status ).to.be.equal( 'inactive' )
        expect( store.getState().buttonText ).to.be.equal( 'Activate' )

        store.dispatch( toggleStatus() )

        expect( store.getState().status ).to.be.equal( 'active' )
        expect( store.getState().buttonText ).to.be.equal( 'Deactivate' )

        store.dispatch( toggleStatus() )

        expect( store.getState().status ).to.be.equal( 'inactive' )
        expect( store.getState().buttonText ).to.be.equal( 'Activate' )

        store.dispatch( toggleStatus() )

        expect( store.getState().status ).to.be.equal( 'active' )
        expect( store.getState().buttonText ).to.be.equal( 'Deactivate' )
    } )
} )

Line by line here is my understanding of “basic Redux”.
The store object keeps the whole application state; there is only a store in the application.
To make something change in the state of the store I need to dispatch a action on it; a action producer is a function that returns an object; in my code the only action producer is the toggleStatus function.
An action must always contain an action.type and may or may not contain some additional information; the toggleStatus function will return a bare essential action object containing only a type.
The store will change its state passing the action to each reducer function; a reducer function takes the current store state, or part of it, as an input and returns an updated version of the passed state.
In my code the status function will take state.status as an input and return a value that will be assigned to state.status; the buttonText function will do the same with the button text.
The store can be initialized with an initial state; so doing will mean the reducers will be immediately applied to it.
The test above passes and Chai makes it easy enough to understand what is going on:

Using the store in a class

Sticking to the metaphor of the store as an “event bus” the next step is trying to subscribe to changes.
If the first test method asserted the store status changes in a predictable way this second test will tell me how I can use that changed information in JavaScript objects.
I’ve created a simple ES6 class that will subscribe to the store changes:

class Logger {
    constructor( store ) {
        this.log = []
        this.store = store
        this.store.subscribe( this.logStatusToggle.bind( this ) )
    }

    logStatusToggle() {
        const status = this.store.getState().status
        this.log.push( status )
    }

    getLog() {
        return this.log
    }
}

And set up another test to make sure I got this right:

it( 'should correctly allow subscribing to state changes', function () {
    const store = createStore( reducer, initialState )

    const logger = new Logger( store )

    store.dispatch( toggleStatus() )
    store.dispatch( toggleStatus() )
    store.dispatch( toggleStatus() )

    expect( logger.getLog().length ).to.be.equal( 3 )
    expect( logger.getLog() ).to.be.eql( ['active', 'inactive', 'active'] )
} )

The test passes as expected:

Adding the store to React components

Now that I’ve got a grasp on some basic Redux mechanics it’s time to connect that to React components.
Redux comes with its own React connecting library but, for the time being, I will not use it as I would like to understand what it might be doing for me.
Since I’ve dealt with status and controls until now I write some React components to use in the next tests:

const Status = function ( {text} ) {
    return (
        <p className='status'>{text}</p>
    )
}

const Control = function ( props ) {
    return (
        <button className='control' onClick={function () {
            props.store.dispatch( toggleStatus() )
        }}>{props.text}</button>
    )
}

const View = function ( props ) {
    return (
        <div>
            <Status text={props.status}/>
            <Control text={props.buttonText} store={props.store}/>
        </div>
    )
}

class Wrapper extends React.Component {
    constructor( props ) {
        super( props )
        this.store = props.store
        this.store.subscribe( this.handleStatusChange.bind( this ) )

        const storeState = this.store.getState()

        this.state = {
            status: storeState.status,
            buttonText: storeState.buttonText,
        }
    }

    handleStatusChange() {
        const storeState = this.store.getState()

        this.setState( {
            status: storeState.status,
            buttonText: storeState.buttonText,
        } )
    }

    render() {
        const viewProps = {
            store: this.store,
            status: this.state.status,
            buttonText: this.state.buttonText,
        }

        return (
            <div>
                <View {...viewProps}/>
            </div>
        )
    }
}

In the code above I define three functional React components and one class based component implementation.
The functional components serve the purpose to render the following HTML structure;

<View>
    <Status/>
    <Control/>
</View>

The components are functional as they are implemented as simple functions in place of being ES6 classes extending the React.Component class; this seems to be good from a performance point of view and makes them very easy to test.
The last component, the Wrapper class, wraps the outer view component injecting the store object in the View component to have it injected, in turn, in the Control component.
The flow of state change and updated will be the following:
1. click the button rendered by the Control component
2. that will dispatch the action generated by the toggleStatus function to the store
3. the store will use the reducers to update its state
4. since the Wrapper component has subscribed to changes to the store.state it will rerender the View, Status and Control components using the new pieces of information

To make sure I’m understanding this correctly I’ve written three more tests:

it( 'should correctly render the initial state of components based on store state', function () {
    const store = createStore( reducer, initialState )

    const wrapper = mount( <Wrapper store={store}/> )

    expect( wrapper.find( '.status' ).text() ).to.be.equal( 'inactive' )
    expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Activate' )
} )

it( 'should correctly render the status after a status toggle in the store', function () {
    const store = createStore( reducer, initialState )

    const wrapper = mount( <Wrapper store={store}/> )

    store.dispatch( toggleStatus() )

    wrapper.update()

    expect( wrapper.find( '.status' ).text() ).to.be.equal( 'active' )
    expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Deactivate' )

    store.dispatch( toggleStatus() )

    wrapper.update()

    expect( wrapper.find( '.status' ).text() ).to.be.equal( 'inactive' )
    expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Activate' )
} )

it( 'should correctly render the status after a button click', function () {
    const store = createStore( reducer, initialState )

    const wrapper = mount( <Wrapper store={store}/> )

    wrapper.find( '.control' ).simulate( 'click' )

    wrapper.update()

    expect( wrapper.find( '.status' ).text() ).to.be.equal( 'active' )
    <a href="http://www.theaveragedev.com/wp-content/uploads/2017/10/third-redux-test.png"><img src="http://www.theaveragedev.com/wp-content/uploads/2017/10/third-redux-test.png" alt="" width="681" height="252" class="aligncenter size-full wp-image-4268" /></a>expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Deactivate' )

    wrapper.find( '.control' ).simulate( 'click' )

    wrapper.update()

    expect( wrapper.find( '.status' ).text() ).to.be.equal( 'inactive' )
    expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Activate' )
} )

The first test is to make sure the initial state, based on the store initial state, allow components to render correctly; the second test partially tests the components interaction updating the store without clicking the button and making sure the status correctly updates.
The final test is for the whole flow: render the components initial state, click the button and make sure the components update correctly.

The whole code

Here is the whole code, all contained in one file, I’ve used to set up and run the tests I’ve shown in this post:

const React = require( 'react' )
const createStore = require( 'redux' ).createStore
const expect = require( 'chai' ).expect
const mount = require( 'enzyme' ).mount

const Status = function ( {text} ) {
    return (
        <p className='status'>{text}</p>
    )
}

const Control = function ( props ) {
    return (
        <button className='control' onClick={function () {
            props.store.dispatch( toggleStatus() )
        }}>{props.text}</button>
    )
}

const View = function ( props ) {
    return (
        <div>
            <Status text={props.status}/>
            <Control text={props.buttonText} store={props.store}/>
        </div>
    )
}

class Wrapper extends React.Component {
    constructor( props ) {
        super( props )
        this.store = props.store
        this.store.subscribe( this.handleStatusChange.bind( this ) )

        const storeState = this.store.getState()

        this.state = {
            status: storeState.status,
            buttonText: storeState.buttonText,
        }
    }

    handleStatusChange() {
        const storeState = this.store.getState()

        this.setState( {
            status: storeState.status,
            buttonText: storeState.buttonText,
        } )
    }

    render() {
        const viewProps = {
            store: this.store,
            status: this.state.status,
            buttonText: this.state.buttonText,
        }

        return (
            <div>
                <View {...viewProps}/>
            </div>
        )
    }
}

const TOGGLE_STATUS = 'TOGGLE_STATUS'

const toggleStatus = function () {
    return {
        type: TOGGLE_STATUS,
    }
}

const status = function ( state = 'inactive', action ) {
    switch ( action.type ) {
        case TOGGLE_STATUS:
            return state === 'inactive' ? 'active' : 'inactive'
        default:
            return state
    }
}

const buttonText = function ( state = 'Deactivate', action ) {
    switch ( action.type ) {
        case TOGGLE_STATUS:
            return state === 'Deactivate' ? 'Activate' : 'Deactivate'
        default:
            return state
    }
}

function reducer( state = {}, action ) {
    return {
        status: status( state.status, action ),
        buttonText: buttonText( state.buttonText, action ),
    }
}

const initialState = {
    status: 'inactive',
    buttonText: 'Activate',
}

class Logger {
    constructor( store ) {
        this.log = []
        this.store = store
        this.store.subscribe( this.logStatusToggle.bind( this ) )
    }

    logStatusToggle() {
        const status = this.store.getState().status
        this.log.push( status )
    }

    getLog() {
        return this.log
    }
}

describe( 'Basic Redux', function () {
    it( 'should correctly change the status store', function () {
        const store = createStore( reducer, initialState )

        expect( store.getState().status ).to.be.equal( 'inactive' )
        expect( store.getState().buttonText ).to.be.equal( 'Activate' )

        store.dispatch( toggleStatus() )

        expect( store.getState().status ).to.be.equal( 'active' )
        expect( store.getState().buttonText ).to.be.equal( 'Deactivate' )

        store.dispatch( toggleStatus() )

        expect( store.getState().status ).to.be.equal( 'inactive' )
        expect( store.getState().buttonText ).to.be.equal( 'Activate' )

        store.dispatch( toggleStatus() )

        expect( store.getState().status ).to.be.equal( 'active' )
        expect( store.getState().buttonText ).to.be.equal( 'Deactivate' )
    } )

    it( 'should correctly allow subscribing to state changes', function () {
        const store = createStore( reducer, initialState )

        const logger = new Logger( store )

        store.dispatch( toggleStatus() )
        store.dispatch( toggleStatus() )
        store.dispatch( toggleStatus() )

        expect( logger.getLog().length ).to.be.equal( 3 )
        expect( logger.getLog() ).to.be.eql( ['active', 'inactive', 'active'] )
    } )

    it( 'should correctly render the initial state of components based on store state', function () {
        const store = createStore( reducer, initialState )

        const wrapper = mount( <Wrapper store={store}/> )

        expect( wrapper.find( '.status' ).text() ).to.be.equal( 'inactive' )
        expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Activate' )
    } )

    it( 'should correctly render the status after a status toggle in the store', function () {
        const store = createStore( reducer, initialState )

        const wrapper = mount( <Wrapper store={store}/> )

        store.dispatch( toggleStatus() )

        wrapper.update()

        expect( wrapper.find( '.status' ).text() ).to.be.equal( 'active' )
        expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Deactivate' )

        store.dispatch( toggleStatus() )

        wrapper.update()

        expect( wrapper.find( '.status' ).text() ).to.be.equal( 'inactive' )
        expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Activate' )
    } )

    it( 'should correctly render the status after a button click', function () {
        const store = createStore( reducer, initialState )

        const wrapper = mount( <Wrapper store={store}/> )

        wrapper.find( '.control' ).simulate( 'click' )

        wrapper.update()

        expect( wrapper.find( '.status' ).text() ).to.be.equal( 'active' )
        expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Deactivate' )

        wrapper.find( '.control' ).simulate( 'click' )

        wrapper.update()

        expect( wrapper.find( '.status' ).text() ).to.be.equal( 'inactive' )
        expect( wrapper.find( '.control' ).text() ).to.be.equal( 'Activate' )
    } )
} )

The test runs from the /tests/redux/Redux.spec.js file, using the mocha --grep Redux command to run the single file, Mocha is bootstrapped by this file and uses this options file.

Next

Before I start working on refactoring the addon code to more decent standards I will take more time to understand how beneficial the use of Redux and React “bridge code” is; I’ve got no doubt about it but I want to understand what is going on “under the hood”.

I appreciate your input