Własny React Hook
Przy tworzeniu własny hooków obowiązują nas te same reguły, co tych wbudowanych: nazwa każdego hooka musi zaczynać się od "use". Hook jest zwykłą funkcją, a w środku niej możemy wywoływać inne funkcje! Dzięki temu kompozycja wielu hooków staje się bajecznie prosta i nie wymaga żadnych skomplikowanych technik. To tylko zwykłe funkcje.
useDocumentTitle
Zacznijmy od czegoś prostego: Hook, który zmienia tytuł strony na podany. Dla przypomnienia, w artykule na temat useEffect
zaimplementowaliśmy to w ten sposób:
useEffect(() => {
document.title = `Nowy tytuł!`;
});
Jest to bardzo prosta, żeby nie powiedzieć naiwna implementacja, ale na pewno spełnia swoje zadanie. Jak stworzyć z niej własny hook? O tak:
const useDocumentTitle = (title) => {
useEffect(() => {
document.title = title;
}, [title]);
};
Następnie w komponencie użyjemy go w taki sposób:
useDocumentTitle('Mój tytuł');
Łał, to było proste, no nie? Stworzyliśmy zwykłą funkcję, w której wywołujemy hook i to tyle.
Dodajmy coś jeszcze, np. przywracanie oryginalnego tytułu, gdy komponent jest odmontowywany:
const useDocumentTitle = (title) => {
const defaultTitle = useRef(document.title); // 1
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => {
// 2
return () => {
document.title = defaultTitle.current;
};
}, []);
};
Tutaj w linijce oznaczonej numerem 1 zapisujemy istniejący document.title
do refa. Następnie w drugim useEffect
zwracamy funkcję, która zostanie wywołana tylko przy odmontowywaniu komponentu i ustawiamy w niej document.title
na oryginalną wartość zapisaną w refie.
usePrevious
Czasem potrzebne nam informacje o poprzedniej wartości danego propsa. O ile w klasach nie było z tym problemu, tak w komponentach funkcyjnych musimy zadbać o to już sami:
const usePrevious = (value) => {
const ref = useRef(); // 1
useEffect(() => {
ref.current = value; // 3
}, [value]);
return ref.current; // 2
};
W tym przypadku tworzymy pusty ref (1), zwracamy poprzednią wartość (2), a następnie zapisujemy do refa nową(3). Dzieje się tak dlatego, że useEffect
uruchamia się asynchronicznie.
Najczęściej jednak zamiast używać usePrevious
, możemy ten sam problem rozwiązać inaczej i prościej, np. poprzez dodanie danego propsa do tablicy zależności useEffect
. Wtedy React sam za nas porówna starą i nową wartość!
useApi
A gdyby tak… w hooku zamknąć pobieranie danych z API? To proste! Weźmy kod podobny do tego z artykułu o Hookach i API i zamieńmy na własny hook, którego będziemy mogli używać w wielu miejscach w naszej aplikacji. Pierwsze podejście wygląda tak:
const useApi = (path) => {
const [response, setResponse] = useState({ data: null, isLoading: true, error: null });
useEffect(() => {
setResponse({ data: null, isLoading: true, error: null });
fetch('https://rickandmortyapi.com/api/' + path)
.then((res) => res.json())
.then((data) => setResponse({ data, isLoading: false, error: null }))
.catch((error) => setResponse({ data: null, isLoading: false, error }));
}, [path, setResponse]);
return response;
};
Nie jest to jeszcze zbyt piękne, ale działa całkiem nieźle:
function App() {
const [page, setPage] = React.useState(10);
const { data, isLoading } = useApi(`character/?page=${page}`);
return (
<>
{isLoading && 'Loading…'}
<button onClick={() => setPage((p) => p + 1)}>Next</button>
<ul>
{data?.results.map((character) => (
<li key={character.id}>{character.name}</li>
))}
</ul>
</>
);
}
Naszego hooka możemy poprawić na dwa sposoby. Po pierwsze, pozbyć się z niego logiki odpowiedzialnej za pobieranie danych z API – tzn. jest to coś całkowicie niezależnego od Reacta. Chcemy wywołać tylko getData(…)
i tyle, a nie martwić się o jakieś res.json()
i inne podobne historie. Przykładowo (upraszczając):
const doFetch = async (path) => {
const res = await fetch('https://rickandmortyapi.com/api/' + path);
if (res.ok) {
return res.json();
}
throw await res.json();
};
Drugą poprawką będzie użycie useReducer
, aby pozbyć się nadmiaru kodu z samego useEffect:
const apiReducer = (state, action) => {
switch (action.type) {
case 'FETCHING':
return { data: null, isLoading: true, error: null };
case 'SUCCESS':
return { data: action.payload, isLoading: false, error: null };
case 'ERROR':
return { data: null, isLoading: false, error: action.payload };
}
return state;
};
const useApi = (path) => {
const [response, dispatch] = useReducer(apiReducer, { data: null, isLoading: false, error: null });
useEffect(() => {
dispatch({ type: 'FETCHING' });
doFetch(path)
.then((data) => dispatch({ type: 'SUCCESS', payload: data }))
.catch((error) => dispatch({ type: 'ERROR', payload: error }));
}, [path]);
return response;
};
Kod jest znacznie dłuższy, ale wydaje mi się też bardziej czytelny, bo oddzielone zostały od siebie elementy niezależnej logiki. Efekt:
Podsumowanie
To tylko kilka podstawowych hooków, zachęcam do tworzenia własnych. To mega proste :) Pochwal się w komentarzu, jakie inne ciekawe hooki znasz!
Jeśli chcesz na bieżąco dowiadywać się o kolejnych częściach kursu React.js to koniecznie śledź mnie na Facebooku i zapisz się na newsletter.