Async Actions

Most complex apps rely heavily on fetching data asynchronously. In this document, we present techniques used in Redux to handle an asynchronous data flow.

Introduction to Middleware and redux-thunk

When using an API, you are probably dealing with asynchronous actions. However, the Redux store only supports synchronous actions without using middleware (more on that later). To use middleware in Redux, we use the applyMiddleware() store enhancer from Redux. redux-thunk middleware is the standard way to handle asynchronous actions.

We use redux-thunk middleware to enable asynchronous requests to work with synchronous action creators. It allows an action creator to return a function instead of an object (action) and executes that function when it is returned. This allows non-pure actions (i.e. ones that can call APIs that might have different data each time). These action creators can dispatch other actions, so, for example, you can dispatch a REQUEST_BEGIN action, then fetch remote data asynchronously and, after it returns, dispatch the REQUEST_SUCCESS or REQUEST_ERROR actions.

For example, you can create an async incrementer as follows:

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  };
}

LS2Request Example

A combination of redux-thunk and LS2Request allows us to fetch and display data in a React component. LS2Request is a wrapper component for PalmServiceBridge and is available from @enact/webos/LS2Request. The following example shows a simple fetch routine.

At the root level, we use <Provider /> to pass store down the component hierarchy.

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import configureStore from './store';
import App from './containers/App';

const store = configureStore();
render(
	<Provider store={store}>
		<App />
	</Provider>,
	document.getElementById('root')
);

Store is configured to accept thunk middleware

import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import systemSettingsReducer from '../reducers';

export default function configureStore (initialState) {
	const store = createStore(
		systemSettingsReducer,
		initialState,
		applyMiddleware(thunkMiddleware) // lets us dispatch functions
	);
	return store;
}

Here we create a thunk action creator which returns a function instead of a plain object. It is also possible to dispatch an action or request at the beginning.

import LS2Request from '@enact/webos/LS2Request';
function receiveSystemSettings (res) {
	return {
		type: 'RECEIVE_SYSTEM_SETTINGS',
		payload: res
	};
}
// function returning function!
export const getSystemSettings = params => dispatch => {
	// possible to dispatch an action at the start of fetching
	// dispatch({type: 'FETCH_SYSETEM_SETTINGS'});
	return new LS2Request().send({
		service: 'luna://com.webos.settingsservice/',
		method: 'getSystemSettings',
		parameters: params,
		onSuccess: (res) => {
			// dispatches action on success callback with payload
			dispatch(receiveSystemSettings(res));
		}
	});
};

Reducer receives a payload and creates a new state.

export default function systemSettingsReducer (state = {}, action) {
	switch (action.type) {
		case 'RECEIVE_SYSTEM_SETTINGS':
			return Object.assign({}, state, action.payload.settings);
		default:
			return state;
	}
}

Connected container dispatches getSystemSettings on componentDidMount and renders a pictureMode prop that’s been hooked up with a redux store.

import React from 'react';
import {connect} from 'react-redux';
import {getSystemSettings} from '../actions';

class App extends React.Component {
	componentDidMount () {
		this.props.dispatch(getSystemSettings({
			category: 'picture',
			key: 'pictureMode',
			subscribe: true
		}));
	}
	render () {
		return <p>{this.props.pictureMode}</p>;
	}
}

export default connect()(App);