Home Adding Redux to a React app
Post
Cancel

Adding Redux to a React app

Why do we need Redux in a React app?

In a large application you will commonly have a large amount of state. The solutions are either to have the state in the most parent component and pass the values down via props or each component has local state.

  • Passing as props becomes problematic on pages with many controls.
  • Components managing own state causes problem if needed to be modified at a different location in the page.

The solution therefore is to use Redux which defines specific Actions and Reducers to manage access to state.

Getting started

  • In Visual Studio Code, open command line to the root of the React project and run npm install redux react-redux
  • Inside the src folder, create store.js
1
2
3
4
5
6
7
import { createStore, combineReducers } from 'redux';
import { todos } from './todos/reducers'; // Include reducers
const reducers = {
    todos,
};
const rootReducer = combineReducers(reducers);
export const configureStore = () => createStore(rootReducer);

Open index.js and add

1
2
import { Provider } from 'react-redux';
import { configureStore } from './store';

Wrap the <App />

1
2
3
4
5
6
ReactDOM.render(
    <Provider store={configureStore()} >
        <App />
    </Provider>,
    document.getElementById('root'),
);

See more in the attended learning course examples -> CH03

To maintain state across browser refreshes use Redux Persist

npm install redux-persist

Update store.js

1
2
3
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
1
2
3
4
5
6
7
8
9
10
11
const rootReducer = combineReducers(reducers);

const persistConfig = {
    key: 'root',
    storage, // Defaults to local storage in the browser
    stateReconciler: autoMergeLevel2,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const configureStore = () => createStore(persistedReducer);

Update index.js

1
2
import { persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/lib/integration/react';
1
2
3
4
5
6
7
8
9
10
11
12
13
const store = configureStore();
const persistor = persistStore(store);

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

Redux DevTools

Add this Chrome extension to make development with Redux easier!

Also update store.js

1
2
3
4
5
6
export const configureStore = () => 
    createStore(
        persistedReducer,
        window.__REDUX_DEVTOOLS_EXTENSION__ &&
        window.__REDUX_DEVTOOLS_EXTENSION__(), // This connects our app to the redux extension
    );

Redux Best Practices

  • Export the connected and unconnected versions of a component
    • export const TodoList = ... So we can test the component as is
    • export default connect(mapStateToProps, mapDispatchToProps)(TodoList); For the application
  • Keep Redux actions and async operations out of your reducers
  • Think carefully about connecting components - ‘Connecting a component can, in practice, make it less reusable.’

Why use a side-effect library

Move data fetching or sending outside of a component, components should only be responsible to displaying or taking user input, into a side effect library like Redux Thunk or Redux Saga. Redux Thunk is a simpler library to learn.

Install and configure Redux Thunk

See CH04 in the example bundle

npm install redux-thunk redux-devtools-extension @babel/runtime

npm install --save-dev @babel/plugin-transform-runtime

open .babelrc file

1
2
3
4
{
    "presets@: ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["@babel/plugin-transform-runtime"]
}

open store.js

1
2
3
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

Update following section

1
2
3
4
5
6
7
export const configureStore = () => 
    createStore(
        persistedReducer,
        composeWithDevTools(
            applyMiddleware(thunk)
        )
    );

Add first simple thunk

Create thunk.js

1
2
3
export const displayAlert = () = () => {
    alert("Hello!");
}

Open a React component and add

1
2
3
4
5
6
7
8
9
10
11
import { displayAlert } from './thunks';

const mapStateToProps = state = ({
    todos: state.todos,
});

const mapDispatchToProps = dispatch => ({
    onRemovePressed: text => dispatch(removeTodo(text)),
    onCompletedPressed: text => dispatch(markTodoAsCompleted(text)),
    onDisplayAlertClicked: () => dispatch(displayAlert()),
})

Async thunk

Add to thunk.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { loadTodosInProgress, loadTodosSuccess, loadTodosFailure} from './actions';


export const loadTodos = () => async (dispatch, getState) => {
    try
    {
        dispatch(loadTodosInProgress());
        const response = await fetch('http://localhost:8080/todos');
        const todos = await response.json();

        dispatch(loadTodosSuccess(todos));
    } catch (e) {
        dispatch(loadTodosFailure());
        dispatch(displayAlert(e));
    }    
}

export const displayAlert = text => () => {
    alert(text);
}

Selectors

Selectors give us a place to put logic for combining, filtering, transforming storing data. This way if/when the way the data is stored in state changes, you only have to update the selectors.js file

See CH05 in the example bundle

Create selectors.js

1
2
export const getToDos = state => state.todos;
export const getToDosLoading = state => state.isLoading;

Replace

1
2
3
4
5
const mapStateToProps = state = ({
    isLoading: state.isLoading,
    todos: state.todos,
});

with

1
2
3
4
5
6
import { getToDos, getToDosLoading } from './selectors';

const mapStateToProps = state = ({
    isLoading: getToDosLoading(state),
    todos: getToDos(state),
});

Reselect

Enables you to create more complicated selectors, additionally it has the extra benefit not re-computing the selector IF the data in state hasn’t changed, it gives the previously computed value.

Install

npm install reselect

1
2
3
4
5
6
7
8
9
import { createSelector } from 'reselect';

export const getToDos = state => state.todos.data;
export const getToDosLoading = state => state.todos.isLoading;

export const getIncompleteTodos = createSelector(
    getTodos,
    (todos) => todos.filter(todo => !todo.isCompleted),
)

The final parameter in the createSelector method gets all the previous values as arguments into the function specified.

1
2
3
4
5
6
7
8
9
10
import { createSelector } from 'reselect';

export const getToDos = state => state.todos.data;
export const getToDosLoading = state => state.todos.isLoading;

export const getIncompleteTodos = createSelector(
    getTodos,
    getTodosLoading,
    (todos, isLoading) => todos.filter(todo => !todo.isCompleted),
)

Styled components

Allow us to define styles inside our JavaScript files

See CH06 in the example bundle

npm install styled-components

1
2
3
4
5
6
import styled from 'styled-components';

const BigRedText = styled.div`
    font-size: 48px;
    color: #ff0000;
`;

Can now be used like any other component

1
<BigRedText>I'm a Styled-Component</BigRedText>

In addition to styled.div can also use

  • styled.h1
  • styled.button
  • styled.(and any other html element name here)

This now means that we can pass props into BigRedText eg.

1
<BigRedText createdDays={todo.numberOfDays}></BigRedText>

Now the style changes based on the numberOfDays

1
2
3
4
5
const BigRedText = styled.div`
    font-size: ${props => (props.createdDays > 4
    ? '48px' : '96px')};
    color: #ff0000;
`;

You can also Inherit styles

1
2
3
4
const BigRedTextWithWarning = styled(BigRedText)`
    color: #ff0000;
    font-size: x-large;
`;

Testing

See CH07 in the example bundle

Setup

npm install --save-dev mocha chai

npm install --save-dev @babel/register

npm install --save-dev sinon node-fetch fetch-mock

Modify package.json

1
"test": "mocha \"src/**/*.test.js\" --require @babel/register --recursive"

npm run test to run test suite.

Testing Reducers

Create tests folder, Create reducers.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { expect } from 'chai';
import { todos } from '../reducers';

describe('The todos reducer', () => {

    it('Adds a new todo where CREATE_TODO action is received', () => {
        const fakeTodo = { text: 'hello', isCompleted: false };
        const fakeAction = {
            type: 'CREATE_TODO',
            payload: {
                todo: fakeTodo,
            },
        };

        const originalState = { isLoading: false, data: [] };

        const expected = {
            isLoading: false,
            data: [fakeTodo],
        };

        const actual = todos(originalState, fakeAction);

        expect(actual).to.deep.equal(expected);
    });
});

Testing Redux Thunks

In tests folder, Create thunks.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import 'node-fetch';
import 'fetchMock' from 'fetch-mock';
import { expect } from 'chai';
import sinon from 'sinon';
import { loadTodos } from '../thunks';

describe('The loadTodos thunk', () => {

    it('Dispatches the correct actions in the success scenario', async () => {
        const fakeDispatch = sinon.spy();

        const fakeTodos = [{ text: '1'}, { text: '2' }];
        fetchMock.get('http://localhost:8080/todos', fakeTodos);

        const expectedFirstAction = { type: 'LOAD_TODOS_IN_PROGRESS' };
        const expectedSecondAction = { 
            type: 'LOAD_TODOS_SUCCESS', 
            payload: {
                todos: fakeTodos,
            }
        };

        await loadTodos()(fakeDispatch);

        expect(fakeDispatch.getCall(0).args[0]).to.deep.equal(expectedFirstAction);
        expect(fakeDispatch.getCall(1).args[0]).to.deep.equal(expectedSecondAction);

        fetchMock.reset(); // Restore to original state
    });

});

Testing Selectors

In tests folder, Create selectors.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { expect } from 'chai';
import { getCompletedTodos } from '../selectors';

describe('The getCompletedTodos selector', () => {

    it('Returns only completed todos', () => {
        const fakeTodos = [{
            text: 'Say hello',
            isCompleted: true
        }, {
            text: 'Say Goodbye',
            isCompleted: false
        }, {
            text: 'Climb Mount Everest',
            isCompleted: false
        }];

        const expected = [{
            text: 'Say hello',
            isCompleted: true
        }];
        
        /// Note the .resultFunc to get the last function which you're testing
        const actual = getCompletedTodos.resultFunc(fakeTodos);

        expect(actual).to.deep.equal(expected);
    });

});

Testing Styled Components

We won’t be testing the style output, what we can test is the function which determines the style output.

Just extract into an export function and test that.

1
2
3
4
5
const BigRedText = styled.div`
    font-size: ${props => (props.createdDays > 4
    ? '48px' : '96px')};
    color: #ff0000;
`;

Convert to

1
2
3
4
5
6
7
export const getStyleForCreated = (days) => (days > 4 ? '48px' : '96px');

const BigRedText = styled.div`
    font-size: ${props => getStyleForCreated(props.createdDays)};
    color: #ff0000;
`;

In tests folder, Create COMPONENT-NAME.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { expect } from 'chai';
import { getStyleForCreated } from '../COMPONENT-NAME';

describe('getStyleForCreated', () => {

    it('Returns 48px when days > 4', () => {
        const actual = getStyleForCreated(10);
        expect(actual).to.equal('48px');
    });

    it('Returns 96px when days <= 4', () => {
        const actual = getStyleForCreated(3);
        expect(actual).to.equal('96px');
    });

});
This post is licensed under CC BY 4.0 by the author.