Retour aux articles

Les 10 meilleurs hooks avec React Router 7 (anciennement Remix)

Ce hook permet de changer un paramètre d'URL sans recharger la page
13 minutes de lecture- 2 vues

Dans cet article, je partage avec vous mes hooks préférés pour React Router 7 (anciennement Remix), que j’utilise dans quasiment tous mes projets.

1. useOptionalUser

C'est le hook que j'utilise le plus. Il permet de récupérer les informations optionnelles d'un utilisateur. Pour le faire fonctionner, il suffit de renvoyer un utilisateur dans le fichier app/routes/root.tsx (fichier racine d'une application Remix). Toute donnée chargée dans root est présente partout dans l'application (comme un contexte global).

useOptionalUser.tsx
1
import { useRouteLoaderData } from "@remix-run/react";
2
import type { loader } from "app/root";
3
4
export const useOptionalUser = () => {
5
const data = useRouteLoaderData<typeof loader>("root");
6
if (data?.user) {
7
return data.user;
8
}
9
return null;
10
};
11
12
export function useUser() {
13
const maybeUser = useOptionalUser();
14
if (!maybeUser) {
15
throw new Error(
16
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.",
17
);
18
}
19
return maybeUser;
20
}

Comme on utilise le type générique du hook useRouteLoaderData, ce hook renvoie une donnée typée. Merci Typescript !

Notez que l'on exporte également un hook useUser qui exploite le hook useOptionalUser, mais qui renvoie une erreur en cas d'utilisateur déconnecter. Cela permet de protéger les pages côté client en renvoyant une erreur en cas d'utilisateur non identifié.

2. useTailwindScreenSizes

J'utilise ce hook pour afficher un bandeau indiquant la taille d'écran active de TailwindCSS. Ces dimensions utilises les valeurs par défaut, qu'on retrouve dans la documentation officielle de TailwindCSS

useTailwindScreenSizes
1
import React from "react";
2
3
export const useTailwindScreenSizes = ({
4
initialWidth = 1280,
5
}: {
6
initialWidth?: number;
7
}) => {
8
const [currentWidth, setCurrentWidth] = React.useState(initialWidth);
9
10
React.useEffect(() => {
11
if (typeof window === "undefined") return;
12
13
const handleScreenResize = () => {
14
const width = window.innerWidth;
15
setCurrentWidth(width);
16
};
17
handleScreenResize();
18
19
window.addEventListener("resize", handleScreenResize);
20
return () => {
21
window.removeEventListener("resize", handleScreenResize);
22
};
23
}, []);
24
25
const getResponsive = () => {
26
if (currentWidth > 1536) {
27
return "2xl";
28
}
29
30
if (currentWidth > 1280) {
31
return "xl";
32
}
33
34
if (currentWidth > 1024) {
35
return "lg";
36
}
37
38
if (currentWidth > 768) {
39
return "md";
40
}
41
if (currentWidth > 640) {
42
return "sm";
43
}
44
45
return "very small";
46
};
47
return { currentWidth, getResponsive };
48
};

J'appelle ensuite ce hook dans un composant nommé DevTool que je place de manière absolue. Dans le fichier root par exemple.

Devtool.tsx
1
import { useIsClient } from "hooks/useIsClient.tsx";
2
import { useTailwindScreenSizes } from "hooks/useTailwindScreenSizes.tsx";
3
4
const Devtool = ({
5
initialScreenWidth = 1280,
6
}: {
7
initialScreenWidth?: number;
8
}) => {
9
const { currentWidth, getResponsive } = useTailwindScreenSizes({
10
initialWidth: initialScreenWidth,
11
});
12
13
const isClient = useIsClient();
14
if (!isClient) return null;
15
return (
16
<div
17
className={`fixed bottom-0 left-0 z-20 flex flex-row gap-2 bg-yellow-500 font-normal text-black`}
18
>
19
<span>{currentWidth}</span>
20
<span>{getResponsive()}</span>
21
</div>
22
);
23
};
24
25
export default Devtool;

3. useIsClient

Ce hook utilise-lui même un hook nommé useIsClient, n'affichant rien côté serveur (on affiche ce composant côté client).

useIsClient.tsx
1
import { useEffect, useState } from "react";
2
3
export function useIsClient() {
4
const [isClient, setClient] = useState(false);
5
6
useEffect(() => {
7
setClient(true);
8
}, []);
9
10
return isClient;
11
}

4. useGetUserPosition

Avez-vous déjà eu besoin de connaître la position géographique des utilisateurs de votre site ? On parle ici de latitude et longitude, qui permettent de calculer la distance kilométrique entre deux points.

C'est notamment utile pour géolocaliser l'utilisateur et lui proposer des données locales (c'est utilisé sur Leboncoin, Airbnb, Uber ...

J'ai utilisé ce hook pour la recherche d'adresse de GoodCollect.

useGetUserPosition.tsx
1
import { useState } from "react";
2
import { useToast } from "components/shadcn-ui/use-toast.ts";
3
4
export const useGetUserPosition = ({
5
successFn = () => {},
6
}: {
7
successFn?: ({ lat, lng }: { lat: number; lng: number }) => void;
8
}) => {
9
const [latLng, setLatLng] = useState<{
10
loading: boolean;
11
latitude: number | null;
12
longitude: number | null;
13
canGeoLocate: boolean;
14
}>({
15
loading: false,
16
latitude: null,
17
longitude: null,
18
canGeoLocate: true,
19
});
20
21
const { toast } = useToast();
22
23
const getUserPosition = () => {
24
if (!navigator.geolocation) {
25
setLatLng((oldValues) => ({
26
...oldValues,
27
canGeoLocate: false,
28
}));
29
return;
30
}
31
setLatLng((oldValues) => ({ ...oldValues, loading: true }));
32
33
navigator.geolocation.getCurrentPosition(
34
(position) => {
35
const { latitude, longitude } = position.coords;
36
37
setLatLng((oldValues) => ({
38
...oldValues,
39
latitude,
40
longitude,
41
loading: false,
42
}));
43
44
successFn?.({ lat: latitude, lng: longitude });
45
},
46
(error) => {
47
if (error.PERMISSION_DENIED) {
48
toast({
49
description:
50
"Votre navigateur n'a pas les permissions pour vous localiser. Vous pouvez l'activer dans les paramètres de votre navigateur.",
51
variant: "error",
52
});
53
} else if (error.TIMEOUT) {
54
toast({
55
description:
56
"La géolocalisation a pris trop de temps. Veuillez réessayer.",
57
variant: "error",
58
});
59
} else if (error.POSITION_UNAVAILABLE) {
60
toast({
61
description:
62
"Votre position n'a pas pu être déterminée. Veuillez réessayer.",
63
variant: "error",
64
});
65
} else {
66
toast({
67
description: "Une erreur est survenue. Veuillez réessayer.",
68
variant: "error",
69
});
70
}
71
setLatLng((oldValues) => ({
72
...oldValues,
73
loading: false,
74
}));
75
},
76
{
77
timeout: 10_000,
78
enableHighAccuracy: true,
79
maximumAge: 60_000, // 60 seconds
80
},
81
);
82
};
83
return { getUserPosition, latLng };
84
};

Ce hook renvoie les valeurs suivantes :

  • la latitude et longitude
  • est-ce que l'utilisateur a accepté d'être géolocalisé / est-ce qu'il peut être géolicalisé
  • est-ce que la géolocalisation est en cours (booléen loading)

Pour connaître la position, nous utilisons l'API native navigator.geolocation.getCurrentPosition.

Ce hook utilise également le hook useToast qui est entièrement facultatif, et qui permet d'afficher une alerte en cas d'erreur. Ce hook est fourni avec le composant toast de la librairie shadcn/ui.

6. useFileUpload

Ce hook permet d'afficher un fichier chargé depuis un input de type file. Le code est à adadpté à votre cas d'utilisation. Le cas d'utilisation n'accepte que les fichiers images ou de type PDF. Il met à disposition plusieurs propriétés pour réinitialiser l'input, afficher le fichier et savoir si c'est une image.

useFileUpload.tsx
1
import React from "react";
2
3
// Takes a file path and returns the file name and extension
4
const checkFilenameFormat = ({
5
fileName,
6
allowedFormats = ["jpg", "jpeg", "png", "heif", "avif", "webp", "pdf"],
7
}: {
8
fileName: string;
9
allowedFormats?: string[];
10
}) => {
11
const fileExtension = fileName.split(".").pop() || ""; // remove the dot
12
13
// Only allow image files
14
const isFormatAllowed = allowedFormats.includes(fileExtension);
15
const isImage = fileExtension !== "pdf";
16
return { isImage, isFormatAllowed, fileExtension };
17
};
18
19
export const useFileUpload = () => {
20
const [fileUpload, setFileUpload] = React.useState<File | null>(null);
21
const [documentPreview, setDocumentPreview] = React.useState<string | null>(
22
null,
23
);
24
25
interface HtmlFormElement extends HTMLInputElement {
26
files: FileList;
27
}
28
const handleFileUpload = (e: React.ChangeEvent<HtmlFormElement>) => {
29
const [file] = e.target.files || [];
30
if (file) {
31
setFileUpload(file);
32
} else {
33
setFileUpload(null);
34
}
35
};
36
37
const isFileImage = React.useMemo(() => {
38
if (!fileUpload) return null;
39
const fileFormat = checkFilenameFormat({
40
fileName: fileUpload?.name || "",
41
});
42
return fileFormat.isImage;
43
}, [fileUpload]);
44
45
// This create a preview of the file, as a URL
46
React.useEffect(() => {
47
if (!fileUpload) return;
48
const objectUrl = URL.createObjectURL(fileUpload);
49
setDocumentPreview(objectUrl);
50
51
return () => URL.revokeObjectURL(objectUrl);
52
}, [fileUpload]);
53
54
const resetFileUpload = () => {
55
setFileUpload(null);
56
setDocumentPreview(null);
57
};
58
59
return {
60
isFileImage,
61
documentPreview,
62
setFileUpload,
63
fileUpload,
64
handleFileUpload,
65
resetFileUpload,
66
};
67
};

Il suffit de l'utiliser avec input de type "file" pour le faire fonctionner.

1
const { documentPreview, handleFileUpload, isFileImage, resetFileUpload } =
2
useFileUpload();
3
4
<input
5
accept="image/*,.pdf"
6
type="file"
7
onChange={handleFileUpload}
8
/>

7. useAddressCombobox

J'ai découvert ce hook en lisant l'article de Kent C. Dodds nommé Full Stack Components. Dedans, il nous explique la notion de composant full-stack : On déclare une route, mais au lieu de renvoyer une vue directement, nous exportons depuis cette route un hook, qui est typé et que nous pouvons ensuite utiliser partout.

Parfait pour un combobox d'adresse autocomplétée par exemple. Je l'ai adaptée pour utiliser l'API de Google (Places API) et obtenir l'auto-complétion des adresses.

Ce hook nécessite de télécharger deux librairies pour fonctionner :

  • Downshift, une librairie que Kent a lui-même mis en place lors de sa mission à Paypal
  • spin-delay pour afficher un spinner (suffisamment longtemps lors des petits chargements)
useAddressCombobox
1
import { useFetcher, useSearchParams } from "@remix-run/react";
2
import { useCombobox } from "downshift";
3
import { useEffect, useId, useState } from "react";
4
import { flushSync } from "react-dom";
5
import type { AddressComboboxType } from "routes/get-address";
6
import { useSpinDelay } from "spin-delay";
7
8
export const useAddressCombobox = ({
9
defaultAddress = null,
10
defaultPlaceId = null,
11
onAddressChange,
12
}: {
13
defaultAddress?: string | null;
14
defaultPlaceId?: string | null;
15
onAddressChange?: ({
16
address,
17
placeId,
18
}: {
19
address: string;
20
placeId: string;
21
}) => void;
22
}) => {
23
const addressFetcher = useFetcher<AddressComboboxType>();
24
const addressAvailabilityFetcher = useFetcher<typeof feedbackLoader>();
25
26
const id = useId();
27
const addresses = addressFetcher.data?.addresses ?? [];
28
type AddressType = (typeof addresses)[number];
29
const [selectedAddress, setSelectedAddress] = useState<
30
null | undefined | AddressType
31
>({
32
address: defaultAddress ?? "",
33
id: defaultPlaceId ?? "",
34
});
35
// An errored address is an address that was suggested by Google, but that is not associated with a zipcode.
36
const [searchParams] = useSearchParams();
37
38
const cb = useCombobox<AddressType>({
39
id,
40
41
onSelectedItemChange: ({ selectedItem }) => {
42
setSelectedAddress(selectedItem);
43
},
44
items: addresses,
45
itemToString: (item) => (item ? item.address : ""),
46
defaultInputValue:
47
defaultAddress ??
48
searchParams.get("address") ??
49
"",
50
onInputValueChange: (changes) => {
51
addressFetcher.submit(
52
{ search: changes.inputValue ?? "" },
53
{ method: "get", action: "/get-address" },
54
);
55
},
56
});
57
58
const busy = addressFetcher.state !== "idle";
59
const showSpinner = useSpinDelay(busy, {
60
delay: 150,
61
minDuration: 500,
62
});
63
const displayMenu = cb.isOpen && addresses.length > 0;
64
65
66
return {
67
displayMenu,
68
showSpinner,
69
selectedAddress,
70
cb,
71
addressFetcher,
72
addresses,
73
};
74
};

8. useDateRanges

Pour les applications nécessitant de manipuler des dates, voici un hook permettant de les formatter sous forme de string. Elles peuvent ensuite être sauvegardées dans l'URL (sur les sites de réservation par exemple).

Nous exploitons dans ce hook la librairie date-fns pour formatter la date.

useDateRanges.tsx
1
2
import {
3
parseStringToDate
4
} from "app/shared/dates.ts";
5
import {
6
differenceInDays
7
} from "date-fns";
8
import React, { useMemo } from "react";
9
import { useSearchParams } from "react-router";
10
11
12
const getDateDifferenceInDays = ({
13
endDate,
14
startDate,
15
}: {
16
endDate: Date;
17
startDate: Date;
18
}) => {
19
// Always add more day, to include the current day
20
const dateDifferenceInDays = differenceInDays(endDate, startDate) + 1;
21
22
return dateDifferenceInDays;
23
};
24
25
export const useDateRanges = ({
26
defaultEndDate = null,
27
defaultStartDate = null,
28
defaultAdditionalDate = null,
29
minDate = null,
30
}: {
31
defaultStartDate?: string | null;
32
defaultEndDate?: string | null;
33
defaultAdditionalDate?: string | null;
34
minDate?: string | null;
35
}) => {
36
const [searchParams] = useSearchParams();
37
38
const startDateParam = searchParams.get("startDate");
39
const endDateParam = searchParams.get("endDate");
40
41
const minimumDate = new Date(minDate) || new Date()
42
43
const [selectedDates, setSelectedDates] = React.useState<{
44
startDate: string;
45
endDate: string;
46
additionalDate?: string;
47
}>(() => {
48
return {
49
startDate: defaultStartDate ?? startDateParam ?? "",
50
endDate: defaultEndDate ?? endDateParam ?? "",
51
additionalDate: defaultAdditionalDate ?? "",
52
};
53
});
54
55
const isOutated = React.useMemo(() => {
56
if (!selectedDates.startDate) return false;
57
const startDate = parseStringToDate(selectedDates.startDate);
58
59
return startDate < minimumDate;
60
}, [selectedDates?.startDate, minimumDate]);
61
62
const dateDifferenceInDays = useMemo(() => {
63
if (!selectedDates.startDate || !selectedDates.endDate) return 0;
64
return getDateDifferenceInDays({
65
endDate: parseStringToDate(selectedDates.endDate),
66
startDate: parseStringToDate(selectedDates.startDate),
67
});
68
}, [selectedDates.endDate, selectedDates.startDate]);
69
70
return {
71
selectedDates,
72
setSelectedDates,
73
minimumDate,
74
isOutated,
75
dateDifferenceInDays,
76
};
77
};

9. useLocalStorage

Ce hook est très pratique pour sauvegarder des informations dans le localStorage

Le localStorage est une base de donnée présente directement dans le navigateur, qui permet par exemple de sauvegarder les informations de l'utilisateur comme son thème, s'il a accepté les cookies, ou toute information utile pour améliorer l'expérience utilisateur.

Cette version du hook nécessite de télécharger la librairie usehooks-ts. Il existe également des alternatives natives à React.

Terminal
1
npm install usehooks-ts
useLocalStorage.tsx
1
import {
2
useCallback,
3
useEffect,
4
useState,
5
type Dispatch,
6
type SetStateAction,
7
} from "react";
8
9
import { useEventCallback, useEventListener } from "usehooks-ts";
10
11
declare global {
12
interface WindowEventMap {
13
"local-storage": CustomEvent;
14
}
15
}
16
17
type SetValue<T> = Dispatch<SetStateAction<T>>;
18
19
export function useLocalStorage<T>(
20
key: string,
21
initialValue: T,
22
): [T, SetValue<T>] {
23
// Get from local storage then
24
// parse stored json or return initialValue
25
const readValue = useCallback((): T => {
26
// Prevent build error "window is undefined" but keeps working
27
if (typeof window === "undefined") {
28
return initialValue;
29
}
30
31
try {
32
const item = window.localStorage.getItem(key);
33
return item ? (parseJSON(item) as T) : initialValue;
34
} catch (error) {
35
console.warn(`Error reading localStorage key “${key}”:`, error);
36
return initialValue;
37
}
38
}, [initialValue, key]);
39
40
// State to store our value
41
// Pass initial state function to useState so logic is only executed once
42
const [storedValue, setStoredValue] = useState<T>(readValue);
43
44
// Return a wrapped version of useState's setter function that ...
45
// ... persists the new value to localStorage.
46
const setValue: SetValue<T> = useEventCallback((value) => {
47
// Prevent build error "window is undefined" but keeps working
48
if (typeof window === "undefined") {
49
console.warn(
50
`Tried setting localStorage key “${key}” even though environment is not a client`,
51
);
52
}
53
54
try {
55
// Allow value to be a function so we have the same API as useState
56
const newValue = value instanceof Function ? value(storedValue) : value;
57
58
// Save to local storage
59
window.localStorage.setItem(key, JSON.stringify(newValue));
60
61
// Save state
62
setStoredValue(newValue);
63
64
// We dispatch a custom event so every useLocalStorage hook are notified
65
window.dispatchEvent(new Event("local-storage"));
66
} catch (error) {
67
console.warn(`Error setting localStorage key “${key}”:`, error);
68
}
69
});
70
71
useEffect(() => {
72
setStoredValue(readValue());
73
// eslint-disable-next-line react-hooks/exhaustive-deps
74
}, []);
75
76
const handleStorageChange = useCallback(
77
(event: StorageEvent | CustomEvent) => {
78
if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) {
79
return;
80
}
81
setStoredValue(readValue());
82
},
83
[key, readValue],
84
);
85
86
// this only works for other documents, not the current one
87
useEventListener("storage", handleStorageChange);
88
89
// this is a custom event, triggered in writeValueToLocalStorage
90
// See: useLocalStorage()
91
useEventListener("local-storage", handleStorageChange);
92
93
return [storedValue, setValue];
94
}
95
96
// A wrapper for "JSON.parse()"" to support "undefined" value
97
function parseJSON<T>(value: string | null): T | undefined {
98
try {
99
// @ts-ignore
100
return value === "undefined" ? undefined : JSON.parse(value ?? "");
101
} catch {
102
return undefined;
103
}
104
}

10. useQueryState

Ce hook s'utilise comme useState, mais va sauvegarder les valeurs comme paramètres d'URL.

Exemple de paramètres d'URL

Dans quel cas vas-tu utiliser les paramètres d'URL ?

  • pour sauvegarder les filtres sur une application de réservation comme leboncoin ou Airbnb.
  • pour la pagination d'un blog (numéro de page sélectionné, limite d'articles affichés).
  • pour sauvegarder la valeur d'une barre de recherche (comme sur Google par exemple)

Pour ce faire, je te présente la librairie nuqs, qui vient exploiter les hooks natifs à Remix (comme useSearchParams)

L'exemple le plus simple se trouve sur la documentation de nuqs

Demo.tsx
1
2
import { useQueryState } from 'nuqs'
3
4
export function Demo() {
5
const [name, setName] = useQueryState('name')
6
return (
7
<>
8
<input value={name || ''} onChange={e => setName(e.target.value)} />
9
<button onClick={() => setName(null)}>Clear</button>
10
<p>Hello, {name || 'anonymous visitor'}!</p>
11
</>
12
)
13
}

L'avantage de ce hook est sa simplicité d'utilisation. L'auteur de la librairie, François Best nous propose une expérience développeur d'exception.

Ses nombreux parsers permettent de valider tout type de donnée (full typesafe) :

  • parseAsString (les paramètres sont des string par défaut)
  • parseAsInteger pour les entiers
  • parseAsFloat pour les chiffres à 2 décimales
  • parseAsBoolean pour les booléens
  • parseAsStringLiteral pour les string literals
  • et bien plus (les dates, les valeurs hexadécimales, les enums ...)

Le hook useQueryState prend comme premier argument le nom du paramètre d'URL. Le deuxième argument est optionel, et peut soit être le parser, soit un objet de configuration :

useQueryState.tsx
1
import { useQueryState, parseAsInteger } from 'nuqs'
2
3
const [search] = useQueryState('search', { defaultValue: '' })
4
// ^? string
5
6
const [count] = useQueryState('count', parseAsInteger)
7
// ^? number | null -> no default value = nullable
8
9
const [count2] = useQueryState('count', parseAsInteger.withDefault(0))
10
// ^? number

J'ai de nombreux paramètres d'URL différents à gérer, comment faire ?

Utilise le hook useQueryStates (au pluriel), pour gérer un objet de plusieurs paramètres d'URL.

1
import { useQueryStates, parseAsFloat } from 'nuqs'
2
3
const [{ latitude, longitude }, setCoordinates] = useQueryStates(
4
{
5
// Use variable names that make sense in your codebase
6
latitude: parseAsFloat.withDefault(45.18),
7
longitude: parseAsFloat.withDefault(5.72)
8
},
9
{
10
urlKeys: {
11
// And remap them to shorter keys in the URL
12
latitude: 'lat',
13
longitude: 'lng'
14
}
15
}
16
)
17
18
// No changes in the setter API, but the keys are remapped to:
19
// ?lat=45.18&lng=5.72
20
setCoordinates({
21
latitude: 45.18,
22
longitude: 5.72
23
})

J'espère que ces hooks vous seront utiles !

Articles similaires

Rejoins la

newsletter