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
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
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.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.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
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
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