Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

How to Implement Data Polling with React, Redux, and Thunk

5.00/5 (2 votes)
17 Apr 2019MIT3 min read 12K  
Example of data polling with React, Redux and Thunk

Introduction

In my previous article, How to load Data in React with Redux-Thunk, Redux-Saga, Suspense & Hooks, I compared different ways of loading data from the API. Quite often in web applications, data needs to be updated frequently to show relevant information to the user. Short polling is one of the ways to do it. Check out this article for more details and alternatives.

Briefly, we are going to ask for new data every N milliseconds. We then show this new data instead of previously loaded data. This article gives an example of how to do it using React, Redux, and Thunk.

Let’s define the problem first.

A lot of components of the web site poll data from the API (for this example, I use public API of iextrading.com to show stock prices) and show this data to the user. Polling logic should be separated from the component and should be reusable. The component should show an error if the call fails and hide previously shown errors if the call succeeded.

This article assumes that you already have some experience with creating React/Redux applications.

The code of this example is available on GitHub.

Project Setup

I assume that you already have a React project created using create-react-app with Redux configured and ready to use. If you have any difficulties with it, you can check out this and/or this.

React-redux-toastr is used for showing a popup with the error.

Store Configuration

Redux store configuration with thunk middleware.

configureStore.js

JavaScript
import { applyMiddleware, createStore, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';

export function configureStore(initialState) {
  return createStore(rootReducer, initialState, compose(applyMiddleware(thunk)));
}

Root Reducer

In rootReducer, we combine reducer with application data (will be created later) and toastr reducer from react-redux-toastr.

rootReducer.js

JavaScript
import {combineReducers} from 'redux';
import data from './reducer';
import {reducer as toastr} from 'react-redux-toastr';

const rootReducer = combineReducers({
    data,
    toastr
});

export default rootReducer;

Actions

We need just one action to load stock prices. If the call succeeded, then LOAD_DATA_SUCCESS action is dispatched to update global state and remove error (if the previous call failed), otherwise, an error is shown.

actions.js

JavaScript
import {toastr} from "react-redux-toastr";
export const LOAD_DATA_SUCCESS = "LOAD_DATA_SUCCESS";

export const loadPrices = () => dispatch => {
    return fetch(
        '<a data-href="https://api.iextrading.com/1.0/stock/market/batch?symbols=aapl,
         fb,tsla,msft,googl,amzn&types=quote'" 
         href="https://api.iextrading.com/1.0/stock/market/batch?symbols=aapl,fb,tsla,
         msft,googl,amzn&types=quote%27" rel="nofollow noopener" target="_blank">
         https://api.iextrading.com/1.0/stock/market/batch?symbols=aapl,fb,tsla,msft,
         googl,amzn&types=quote'</a>)
        .then(response => {
            if (response.ok) {
                return response.json();
            }

            throw new Error(response.statusText);
        })
        .then(
            data => {
                toastr.removeByType('error');
                dispatch({type: LOAD_DATA_SUCCESS, data});
            },
            error => {
                toastr.error(`Error loading data: ${error.message}`);
            })
};

Application Reducer

Reducer updates the global state.

reducer.js

JavaScript
import {LOAD_DATA_SUCCESS} from "./actions";

const initialState = {
    prices: []
};

export default function reducer(state = initialState, action) {
    switch (action.type) {
        case LOAD_DATA_SUCCESS: {
            return {
                ...state,
                prices: action.data
            }
        }
        default: {
            return state;
        }
    }
}

High Order Component (HOC) to query data

It’s probably the most tricky part. HOC, like connect from Redux, can decorate another component to add some additional functionality. In this case, withPolling receives pollingAction and duration as properties. On componentDidMount component calls pollingAction and schedules calling the same action every n milliseconds (2000 by default). On componentWillUnmount component stops polling. More details about HOC here.

withPolling.js

JavaScript
import * as React from 'react';
import {connect} from 'react-redux';

export const withPolling = (pollingAction, duration = 2000) => Component => {
    const Wrapper = () => (
        class extends React.Component {
            componentDidMount() {
                this.props.pollingAction();
                this.dataPolling = setInterval(
                    () => {
                        this.props.pollingAction();
                    },
                    duration);
            }

            componentWillUnmount() {
                clearInterval(this.dataPolling);
            }

            render() {
                return <Component {...this.props}/>;
            }
        });

    const mapStateToProps = () => ({});

    const mapDispatchToProps = {pollingAction};

    return connect(mapStateToProps, mapDispatchToProps)(Wrapper())
};

Example of Usage (PricesComponent)

PricesComponent use withPolling, maps prices from state to props and shows data.

PricesComponent.js

JavaScript
import * as React from 'react';
import {connect} from 'react-redux';
import {loadPrices} from "./actions";
import {withPolling} from "./withPolling";

class PricesComponent extends React.Component {
    render() {
        return (
            <div>
                <table>
                    <thead>
                    <tr>
                        <th>Symbol</th>
                        <th>Company Name</th>
                        <th>Sector</th>
                        <th>Open</th>
                        <th>Close</th>
                        <th>Latest</th>
                        <th>Updated</th>
                    </tr>
                    </thead>
                    <tbody>
                    {Object.entries(this.props.prices).map(([key, value]) => (
                        <tr key={key}>
                            <td>{key}</td>
                            <td>{value.quote.companyName}</td>
                            <td>{value.quote.sector}</td>
                            <td>{value.quote.open}</td>
                            <td>{value.quote.close}</td>
                            <td>{value.quote.latestPrice}</td>
                            <td>{(new Date(Date(value.quote.latestUpdate))).toLocaleTimeString()}</td>
                        </tr>
                    ))}
                    </tbody>
                </table>
            </div>
        );
    }
}

const mapStateToProps = state => ({
    prices: state.data.prices
});

const mapDispatchToProps = {};

export default withPolling(loadPrices)(
    connect(mapStateToProps, mapDispatchToProps)(PricesComponent));

Application

The application consists of PricesComponent and ReduxToastr (component to show errors).

App.js

JavaScript
import React, {Component} from 'react';
import ReduxToastr from 'react-redux-toastr'
import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'
import PricesComponent from "./PricesComponent";

class App extends Component {
    render() {
        return (
            <div>
                <PricesComponent text='My Text'/>
                <ReduxToastr
                    transitionIn="fadeIn"
                    transitionOut="fadeOut"
                    preventDuplicates={true}
                    timeOut={99999}
                />
            </div>
        );
    }
}

export default App;

The app looks like this with no error:

Image 1

And like this with an error. Previously loaded data still shown.

Image 2

withPolling HOC Testing

Enzyme with enzyme-adapter-react-16 is used for testing. The tricky parts here are using jest’s fake timer, creating testAction and WrapperComponent.

setupTests.js

JavaScript
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

// React 16 Enzyme adapter
Enzyme.configure({ adapter: new Adapter() });

withPolling.test.js

JavaScript
import * as React from 'react';
import {mount} from 'enzyme';
import {withPolling} from './withPolling';
import {configureStore} from "./configureStore";
import {Provider} from "react-redux";

jest.useFakeTimers();

describe('withPolling HOC Tests', () => {
    let store;
    let wrapper;

    const TestComponent = () => (
        <div id='test-component'>
            Test Component
        </div>
    );

    beforeEach(() => {
        store = configureStore();
    });

    afterEach(() => {
        wrapper.unmount();
    });

    it('function is called on mount', () => {
        const mockFn = jest.fn();
        const testAction = () => () => {
            mockFn();
        };

        const WrapperComponent = withPolling(testAction)(TestComponent);

        wrapper = mount(<Provider store={store}><WrapperComponent/></Provider>);

        expect(wrapper.find('#test-component')).toHaveLength(1);
        expect(mockFn.mock.calls.length).toBe(1);
    });

    it('function is called second time after duration', () => {
        const mockFn = jest.fn();
        const testAction = () => () => {
            mockFn();
        };

        const WrapperComponent = withPolling(testAction, 1000)(TestComponent);

        wrapper = mount(<Provider store={store}><WrapperComponent/></Provider>);

        expect(wrapper.find('#test-component')).toHaveLength(1);
        expect(mockFn.mock.calls.length).toBe(1);

        jest.runTimersToTime(1001);

        expect(mockFn.mock.calls.length).toBe(2);
    });
});

Conclusion

This example shows how data polling can be implemented using React, Redux, and Thunk.

As an alternative solution, withPolling can be a class component (Polling for example). In this case, <Polling /> will need to be added to the PricesComponent. I think the solution provided in this article is a bit better because we don’t need to add fake components to the JSX (components that don’t add anything visible) and HOC is a technique to add reusable component logic.

That’s it. Enjoy!

License

This article, along with any associated source code and files, is licensed under The MIT License