Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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

DZone 's Guide to

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

We learn how to apply the concept of polling to the process of brining in data to a web application using React, Redux, and Thunk.

· Web Dev Zone ·
Free Resource

Introduction

In my previous article, Loading Data in React: Redux-Thunk, Redux-Saga, Suspense, and HooksI 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 the 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 a web site poll data from an API (for this example, I'm using the 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.

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

Below is the code for Redux store configuration with Thunk middleware.

configureStore.js

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 (which will be created later) and the  toastr reducer from react-redux-toastr.

rootReducer.js

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 the   LOAD_DATA_SUCCESS  action is dispatched to update the global state and remove error (if the previous call failed), otherwise, an error is shown.

actions.js

import {toastr} from "react-redux-toastr";

export const LOAD_DATA_SUCCESS = "LOAD_DATA_SUCCESS";

export const loadPrices = () => dispatch => {
    return fetch(
        'https://api.iextrading.com/1.0/stock/market/batch?symbols=aapl,fb,tsla,msft,googl,amzn&types=quote')
        .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

The below code causes Reducer to update the global state.

reducer.js

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

This is 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 , the component stops polling. More details about HOC can be found  here.

withPolling.js

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 uses  withPolling to map prices from state to props and show data.

PricesComponent.js

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 the   PricesComponent  and   ReduxToastr (the component used to show errors).

App.js

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;


And it will look like like this when an error occurs. Note that the previously loaded data still shown.

withPolling HOC Testing

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

setupTests.js

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

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

withPolling.test.js

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!

Topics:
redux ,web dev ,react.js tutorial ,redux tutorial ,data polling

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}