Skip to content

Lire et modifier des données ​

Presque tous les écrans utilisent deux hooks : useQuery pour lire, useAction pour modifier.

Lire : useQuery ​

ts
const { data, error, loading, reload } = useQuery(() => fetchNotifications(), []);
RenvoieContenu
dataLe résultat, ou null tant que rien n'est arrivé
errorL'erreur de la dernière tentative, ou null
loadingtrue 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 ​

ts
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 ​

ts
// 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 ​

ts
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 :

  1. busy = true (pour désactiver le bouton et éviter les doubles appuis) ;
  2. lance l'action ;
  3. si elle réussit : recharge l'annuaire, affiche le message de succès, renvoie true ;
  4. si elle échoue : affiche le message d'erreur (ou « Échec » par défaut), renvoie false ;
  5. busy = false.
ts
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 :

tsx
<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 ».

ts
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.

RenvoieContenu
status'off', 'on' ou 'break'
shiftLa période en cours, ou null
currentBreakLa pause en cours, ou null
todayShiftsLes périodes d'aujourd'hui
workedMinutes, breakMinutesLes totaux du jour, mis à jour chaque seconde
lastClockOutL'heure du dernier départ
sendingtrue 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).

ts
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/data et gardé en cache pour la session.

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.

Le schéma complet ​