Understanding React and Redux with tests - 01
October 18, 2017
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:
[](https://theaveragedev.com/wp-content/uploads/2017/10/first-redux-test.png)
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:
[](https://theaveragedev.com/wp-content/uploads/2017/10/second-redux-test.png)
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="https://theaveragedev.com/wp-content/uploads/2017/10/third-redux-test.png"><img src="https://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.
[](https://theaveragedev.com/wp-content/uploads/2017/10/third-redux-test.png)
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".