Implementation
Network requests are triggered by React components which need remote resources. These components use some loadSomethingIfNeeded
like function in their useEffect()
hooks. For instance, the PageQuestionnaire
component calls loadCampaignsIfNeeded
when it is mounted. This section explains how this process works.
Network requests
The network requests are written in the src/js/utils/remote-api.js file. They rely on the fetch API. A polyfill is provided for browsers which do not support it, via the babel-polyfill.
Example:
/**
* Retrieve questionnaire
* path like '/pogues/questionnaire:id'
*/
export const getQuestionnaire = id =>
fetch(urlGetQuestionnaire + '/' + id, {
headers: {
Accept: 'application/JSON',
},
}).then(res => res.JSON());
This module exports some functions which return a Promise. It the remote call succeeds, the Promise will resolve to:
- the expected raw data (
res.JSON()
) forGET
operations; - the raw response if no data needs to be extracted from the response;
- value of a header field if relevant.
This file mixes calls for all of the three services:
- visualization (
stromaePostQuestionnaire
); - persistence (
getQuestionnaire
,putQuestionnaire
,getQuestionnaireList
...); - repository (
getCodeListSpecs
,getCodeList
).
Action creators
To trigger remote calls, we use action creators in combination with Redux Thunk middleware. We follow the pattern for asyncrhonous actions from the Redux documentation.
With Redux Thunk, action creators can return a function instead of a plain JavaScript object. Hence, we can write asynchronous actions with a function which:
- synchronously dispatches a plain JavaScript object action to indicate that the request has been registered; action type follows the
LOAD_SOMETHING
naming convention; - sends the request (with functions from the aforementioned src/js/utils/remote-api.js file, like
getQuestionnaire
); - asynchronously dispatches a
LOAD_SOMETHING_SUCCESS
action if the Promise returned by the fetch call succeeds (then
handler), or aLOAD_SOMETHING_FAILURE
if it fails (catch
handler); - returns the Promise for possible further processing.
Example from src/js/actions/questionnaire.js (in the initial code we use arrow functions instead of the regular function
definitions shown here):
import { getQuestionnaire } from '(...)/remote-api';
export function loadQuestionnaire(id) {
//Thanks to Redux Thunk, we can return a function from our action creator.
//This function will be passed two arguments by the Redux Thunk middleware:
//a `dispatch` function to dispatch to the store, and a `getState` function
//(not used here) to read the current application state.
return function (dispatch, getState) {
dispatch({
type: LOAD_QUESTIONNAIRE,
payload: id,
});
return getQuestionnaire(id)
.then(qr => {
dispatch(loadQuestionnaireSuccess(id, questionnaireToState(qr)));
})
.catch(err => {
dispatch(loadQuestionnaireFailure(id, err.toString()));
});
};
}
export function loadQuestionnaireSuccess(id, update) {
return {
type: LOAD_QUESTIONNAIRE_SUCCESS,
payload: { id, update },
};
}
export function loadQuestionnaireFailure(id, err) {
return {
type: LOAD_QUESTIONNAIRE_FAILURE,
payload: { id, err },
};
}
Note: we should probably avoid usage of catch
here (see #146)
TODO screenshot devtools
Do not ask twice for the same resource
We also use Redux Thunk to define some action creators that will take care of not loading a resource again if it is available locally from the application state:
export function loadQuestionnaireIfNeeded(id) {
return function (dispatch, getState) {
const state = getState();
const qr = state.questionnaireById[id];
if (!qr) return dispatch(loadQuestionnaire(id));
};
}
These loadSomethingIfNeeded
action creators can be called from React componentWillMount
and componentWillReceiveProps
life cycle methods.
Example from the src/js/components/questionnaire-container.js file:
class QuestionnaireContainer extends Component {
constructor(props) {
super(props)
}
componentWillMount() {
this.props.loadQuestionnaireIfNeeded(this.props.qrId)
}
componentWillReceiveProps(nextProps) {
if (nextProps.qrId !== this.props.qrId)
this.props.loadQuestionnaireIfNeeded(nextProps.qrId)
}
...
}