Implémentation
Les requêtes sont déclenchées par les composants React qui requièrent des ressources distantes. Ces composants utilisent les fonctions du type loadSomethingIfNeeded
au sein de hooks useEffect()
React. Par exemple, le composant PageQuestionnaire
appelle loadCampaignsIfNeeded
lorsqu'il est instancié. Ce chapitre décrit comment ce processus fonctionne.
Appels distants
Les appels sont définis dans le fichier src/utils/remote-api.js. Ils s'appuient sur l'API fetch. Un polyfill est utilisé pour les navigateurs qui ne supportent pas cette API, grâce à babel-polyfill.
Exemple:
/**
* Récupérer un questionnaire
* chemin du type '/pogues/questionnaire/{id}'
*/
export const getQuestionnaire = async (id, token) => {
const b = await getBaseURI();
return fetch(`${b}/${pathQuestionnaire}/${id}`, {
headers: getHeaders({ Accept: 'application/json' }, token),
}).then(res => res.json());
};
Ce module exporte des fonctions qui retournent une Promesse. Si la requête est réussie, la Promesse sera tenue avec comme valeur:
- les données brutes attendues (
res.JSON()
) pour les requêtes de typeGET
; - la réponse HTTP brute s'il n'y a pas de donnée à extraire de la réponse;
- la valeur d'un champ des headers si opportun.
Ce fichier comporte les appels pour les 3 services utilisés par Pogues:
- visualisation (
visualizeDDI
,visualizePdf
,...); - persistance (
getQuestionnaire
,putQuestionnaire
,getQuestionnaireList
...);
Créateurs d'actions
Pour déclencher un appel distant, nous utilisons les créateurs d'actions avec le middleware Redux Thunk. On applique le shéma pour les appels asynchrones décrits dans la documentation de Redux.
Grâce à Redux Thunk, les créateurs d'actions peuvent retourner une fonction au lieu d'un objet JavaScript
. Ainsi, on peut écrire des actions asyncrhones avec une fonction qui:
- envoie immédiatement au store une action sous la forme d'un objet
JavaScript
, pour indiquer que la requête a bien été enregistrée; le type d'action prend la formeLOAD_SOMETHING
; - envoie la requête (grâce aux utilitaires mentionnés plus haut, par exemple la fonction
getQuestionnaire
); - envoie au store de façon différée une action de la forme
LOAD_SOMTHING_SUCCESS
si la Promesse renvoyée par fetch a été résolue (le gestionnairethen
défini dans la Promesse), ou une action de la formeLOAD_QUESTIONNAIRE_FAILURE
si elle échoue (gestionnairecatch
); - retourne la Promesse pour d'éventuelles valorisations ultérieures.
Exemple à partir du fichier src/actions/questionnaire.js (dans le code initial, nous utilisons des fonctions flêchées à la place des fonctions classiques):
import { getQuestionnaire } from '(...)/remote-api';
export const loadQuestionnaire = (id, token) => dispatch => {
dispatch(loadQuestionnaireStart());
dispatch({
type: LOAD_QUESTIONNAIRE,
payload: id,
});
return getQuestionnaire(id, token)
.then(qr => {
dispatch(loadQuestionnaireSuccess(questionnaireRemoteToStores(qr)));
})
.catch(err => {
dispatch(loadQuestionnaireFailure(id, err));
});
};
export const loadQuestionnaireStart = () => ({
type: LOAD_QUESTIONNAIRE_START,
payload: {},
});
export const loadQuestionnaireSuccess = update => ({
type: LOAD_QUESTIONNAIRE_SUCCESS,
payload: {
update,
},
});
export const loadQuestionnaireFailure = (id, err) => ({
type: LOAD_QUESTIONNAIRE_FAILURE,
payload: { id, err },
});
Remarque: on devrait vraisemblablement éviter l'usage de catch
ici (cf. #146)
TODO screenshot devtools
Ne pas récupérer deux fois la même ressource
Nous utilisons également Redux Thunk pour définir des créateurs d'actions qui veilleront à ne pas effectuer un appel distant pour ressource qui aurait déjà été récupérée, et qui serait donc disponible localement dans l'état de l'application.
export const loadUnitsIfNeeded = token => (dispatch, getState) => {
const state = getState();
const { units } = state.metadataByType;
if (!units) dispatch(loadUnits(token));
};
Les créateurs d'actions du type loadSomethingIfNeeded
sont appelés à partir des hooks useEffect()
des composants React qui ont besoin de la ressource Something
.
Exemple issu du fichier src/layout/page-questionnaire/components/page-questionnaire.jsx:
const PageQuestionnaire = props => {
...
useEffect(() => {
if (
activeQuestionnaire &&
!isEqual(activeQuestionnaire, activeQuestionnaireState)
) {
if (activeQuestionnaire.campaigns) {
const idCampaign =
activeQuestionnaire.campaigns[
activeQuestionnaire.campaigns.length - 1
];
loadStatisticalContext(idCampaign, token);
}
if (activeQuestionnaire.operation) {
loadCampaignsIfNeeded(activeQuestionnaire.operation, token);
}
setActiveQuestionnaireState(activeQuestionnaire);
}
}, [
token,
activeQuestionnaire,
activeQuestionnaireState,
loadStatisticalContext,
loadCampaignsIfNeeded,
]);
...
}