Lire et modifier des données
Presque tous les écrans utilisent deux hooks : useQuery pour lire, useAction pour modifier.
Lire : useQuery
const { data, error, loading, reload } = useQuery(() => fetchNotifications(), []);| Renvoie | Contenu |
|---|---|
data | Le résultat, ou null tant que rien n'est arrivé |
error | L'erreur de la dernière tentative, ou null |
loading | true pendant le chargement |
reload() | Relance le chargement à la demande |
Le deuxième argument ([], [user.id]...) est la liste des dépendances : si l'une change, la fonction est relancée.
Comment il marche
export function useQuery<T>(load: () => Promise<T>, deps: unknown[]) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<unknown>(null);
const [loading, setLoading] = useState(true);
const latest = useRef(0);
const run = useCallback(load, deps);
const reload = useCallback(async () => {
const call = ++latest.current;
setLoading(true);
try {
const result = await run();
if (call === latest.current) {
setData(result);
setError(null);
}
} catch (failure) {
if (call === latest.current) setError(failure);
} finally {
if (call === latest.current) setLoading(false);
}
}, [run]);
useFocusEffect(useCallback(() => { reload(); }, [reload]));
return { data, error, loading, reload };
}Trois choix à comprendre :
1. Il recharge à chaque fois que l'écran s'affiche.useFocusEffect (d'expo-router) lance reload() quand l'écran devient visible : à l'ouverture, et chaque fois qu'on y revient. Si un manager a corrigé tes heures sur le web, il suffit de revenir sur l'onglet pour les voir.
2. Il garde les anciennes données pendant le rechargement.data n'est pas remis à null au début d'un reload(). L'écran affiche les anciennes valeurs, puis les nouvelles. Pas de clignotement ni d'écran vide.
3. Il ignore les réponses en retard. Si deux chargements se croisent (on change vite de semaine), le compteur latest permet de ne garder que la réponse du dernier appel. Une vieille réponse arrivée après coup est ignorée.
Exemples réels
// src/app/(tabs)/notifications.tsx
const { data } = useQuery(fetchNotifications, []);
// src/app/(tabs)/change-email.tsx
const { data: pending, reload } = useQuery(() => fetchPendingEmailChange(user.id), [user.id]);
// src/hooks/use-today.ts : deux requêtes en parallèle
const query = useQuery(async () => {
const today = startOfDay(new Date());
const [periods, lastClockOut] = await Promise.all([
fetchWorkingTimes(user.id, addDays(today, -1), addDays(today, 1)),
fetchLastClockOut(user.id),
]);
return { periods, lastClockOut };
}, [user.id]);Modifier : useAction
const { busy, run } = useAction();
const ok = await run(
() => requestEmailChange(user.id, value, password), // l'action
t('settings.changeEmail.sent', { email: value }), // message si ça marche
(error) => fieldError(error, 'email') // message si ça échoue
? t('settings.changeEmail.taken', { email: value })
: t('common.failed'),
);
if (ok) reload();run fait, dans l'ordre :
busy = true(pour désactiver le bouton et éviter les doubles appuis) ;- lance l'action ;
- si elle réussit : recharge l'annuaire, affiche le message de succès, renvoie
true; - si elle échoue : affiche le message d'erreur (ou « Échec » par défaut), renvoie
false; busy = false.
export function useAction() {
const { t } = useTranslation();
const showToast = useToast();
const { reload } = useDirectory();
const [busy, setBusy] = useState(false);
const run = async (action, success?, failure?) => {
setBusy(true);
try {
await action();
await reload().catch(() => {});
if (success) showToast(success);
return true;
} catch (error) {
showToast(failure?.(error) ?? t('common.failed'));
return false;
} finally {
setBusy(false);
}
};
return { busy, run };
}Pourquoi recharger l'annuaire après chaque action ? Parce que beaucoup d'actions le modifient (nouvelle équipe, rôle changé, membre retiré) et qu'il vaut mieux une requête de trop qu'un écran faux.
Le bouton s'écrit alors :
<Button label={t('settings.changeEmail.submit')} disabled={!valid || busy} onPress={submit} />Les autres hooks
useNow(interval)
Renvoie une Date qui se met à jour toute seule. Sert aux chronomètres et aux calculs « jusqu'à maintenant ».
const now = useNow(); // chaque seconde (écran Clock)
const now = useNow(60 * 1000); // chaque minute (historique, tableau de bord)Il se met en pause quand l'écran n'est pas visible (useIsFocused). Un onglet caché ne consomme pas de batterie à recalculer un chrono que personne ne regarde.
useToday()
Tout ce dont l'onglet Clock et l'écran Today ont besoin. Voir Exemple complet : pointer.
| Renvoie | Contenu |
|---|---|
status | 'off', 'on' ou 'break' |
shift | La période en cours, ou null |
currentBreak | La pause en cours, ou null |
todayShifts | Les périodes d'aujourd'hui |
workedMinutes, breakMinutes | Les totaux du jour, mis à jour chaque seconde |
lastClockOut | L'heure du dernier départ |
sending | true pendant l'envoi d'un pointage |
toggleShift(), toggleBreak() | Les actions de pointage |
useHistory()
Les périodes et les corrections des 4 dernières semaines (la semaine en cours et les 3 précédentes), transformées en une ligne par jour avec un statut. Utilisé par Times et par le formulaire de correction (pour proposer les jours).
const { days, todayPeriods, now } = useHistory();
// days : [{ workDate, start, end, breakMinutes, workedMinutes, status }, ...]Le calcul est fait par describeHistory dans src/utils/history.ts. Voir Règles métier.
useTeamName()
Le nom de l'équipe de l'utilisateur, affiché sur l'onglet Clock et dans Compte.
- Manager et GM : cherché dans l'annuaire.
- Employé : l'annuaire ne contient pas d'équipe, donc le nom est lu une fois dans
/users/:id/dataet gardé en cache pour la session.
usePasswordLink()
Vérifie le jeton d'un lien d'invitation ou de réinitialisation. Voir Authentification.
useRefreshDirectory()
Recharge l'annuaire chaque fois que l'écran s'affiche. Utilisé sur les écrans qui listent des personnes ou des équipes.