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
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
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
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:
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
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
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
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
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:
And like this with an error. 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, creating testAction
and WrapperComponent
.
setupTests.js
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
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!