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.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
In my previous article, Loading Data in React: Redux-Thunk, Redux-Saga, Suspense, and 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 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
React-redux-toastr is used for showing a popup with the error.
Store Configuration
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
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
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
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
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!
Opinions expressed by DZone contributors are their own.
Comments