Przydatne haki React, których możesz użyć w swoich projektach
Opublikowany: 2022-03-10Hooki to po prostu funkcje, które pozwalają podpiąć się pod lub skorzystać z funkcji Reacta. Zostały one wprowadzone na konferencji React Conf 2018, aby rozwiązać trzy główne problemy komponentów klas: piekło opakowujące, ogromne komponenty i mylące klasy. Hooki dają moc komponentom funkcjonalnym Reacta, dzięki czemu można za ich pomocą stworzyć całą aplikację.
Wspomniane wcześniej problemy komponentów klasowych są ze sobą połączone i rozwiązanie jednego bez drugiego mogłoby spowodować dalsze problemy. Na szczęście hooki rozwiązały wszystkie problemy w prosty i skuteczny sposób, jednocześnie tworząc miejsce na ciekawsze funkcje w React. Hooki nie zastępują już istniejących koncepcji i klas Reacta, a jedynie udostępniają API umożliwiające bezpośredni dostęp do nich.
Zespół React wprowadził kilka hooków w React 16.8. Jednak możesz również użyć haków od dostawców zewnętrznych w swojej aplikacji, a nawet utworzyć niestandardowy hak. W tym samouczku przyjrzymy się kilku przydatnym hookom w React i sposobom ich użycia. Przejrzymy kilka przykładów kodu każdego haka, a także zbadamy, jak utworzyć niestandardowy hak.
Uwaga: ten samouczek wymaga podstawowej wiedzy o JavaScript (ES6+) i React.
Motywacja za haczykami
Jak wspomniano wcześniej, haki zostały stworzone, aby rozwiązać trzy problemy: piekło opakowujące, ogromne komponenty i mylące klasy. Przyjrzyjmy się każdemu z nich bardziej szczegółowo.
Piekło opakowania
Złożone aplikacje zbudowane z komponentów klasy łatwo wpadają w owijające piekło. Jeśli przyjrzysz się aplikacji w React Dev Tools, zauważysz głęboko zagnieżdżone komponenty. To bardzo utrudnia pracę z komponentami lub ich debugowanie. Chociaż te problemy można rozwiązać za pomocą komponentów wyższego rzędu i właściwości renderujących , wymagają one niewielkiej modyfikacji kodu. Może to prowadzić do zamieszania w złożonej aplikacji.
Hooki są łatwe do współdzielenia, nie musisz modyfikować swoich komponentów przed ponownym użyciem logiki.
Dobrym tego przykładem jest użycie komponentu Redux connect
Higher Order Component (HOC) do subskrypcji sklepu Redux. Podobnie jak wszystkie KWR, aby użyć connect KWR, musisz wyeksportować komponent wraz ze zdefiniowanymi funkcjami wyższego rzędu. W przypadku connect
będziemy mieli coś z tej formy.
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
Gdzie mapStateToProps
i mapDispatchToProps
to funkcje, które należy zdefiniować.
Podczas gdy w erze hooków można łatwo i zwięźle osiągnąć ten sam wynik, używając useSelector
i useDispatch
Redux.
Ogromne komponenty
Składniki klasy zwykle zawierają efekty uboczne i logikę stanową. Wraz ze wzrostem złożoności aplikacji często zdarza się, że składnik staje się bałaganiarski i zagmatwany. Dzieje się tak, ponieważ oczekuje się, że skutki uboczne będą zorganizowane według metod cyklu życia, a nie funkcjonalności. Chociaż możliwe jest rozdzielenie komponentów i uproszczenie ich, często wprowadza to wyższy poziom abstrakcji.
Hooki organizują efekty uboczne według funkcjonalności i możliwe jest podzielenie komponentu na części w oparciu o funkcjonalność.
Mylące klasy
Klasy są na ogół trudniejszym pojęciem niż funkcje. Komponenty oparte na klasach React są gadatliwe i nieco trudne dla początkujących. Jeśli jesteś nowy w JavaScript, możesz znaleźć funkcje łatwiejsze do rozpoczęcia ze względu na ich lekką składnię w porównaniu z klasami. Składnia może być myląca; czasami można zapomnieć o powiązaniu procedury obsługi zdarzeń, która może złamać kod.
React rozwiązuje ten problem za pomocą funkcjonalnych komponentów i zaczepów, pozwalając programistom skupić się na projekcie, a nie na składni kodu.
Na przykład, następujące dwa komponenty React dadzą dokładnie ten sam wynik.
import React, { Component } from "react"; export default class App extends Component { constructor(props) { super(props); this.state = { num: 0 }; this.incrementNumber = this.incrementNumber.bind(this); } incrementNumber() { this.setState({ num: this.state.num + 1 }); } render() { return ( <div> <h1>{this.state.num}</h1> <button onClick={this.incrementNumber}>Increment</button> </div> ); } }
import React, { useState } from "react"; export default function App() { const [num, setNum] = useState(0); function incrementNumber() { setNum(num + 1); } return ( <div> <h1>{num}</h1> <button onClick={incrementNumber}>Increment</button> </div> ); }
Pierwszy przykład to składnik oparty na klasach, a drugi to składnik funkcjonalny. Chociaż jest to prosty przykład, zauważ, jak fałszywy jest pierwszy przykład w porównaniu z drugim.
Konwencja i zasady dotyczące haków
Zanim zagłębimy się w różne haki, warto przyjrzeć się konwencji i zasadom, które mają do nich zastosowanie. Oto kilka zasad dotyczących haków.
- Konwencja nazewnictwa hooków powinna zaczynać się od prefiksu
use
. Możemy więc miećuseState
,useEffect
, itp. Jeśli używasz nowoczesnych edytorów kodu, takich jak Atom i VSCode, wtyczka ESLint może być bardzo przydatną funkcją dla haków React. Wtyczka zawiera przydatne ostrzeżenia i wskazówki dotyczące najlepszych praktyk. - Hooki muszą być wywoływane na najwyższym poziomie komponentu, przed instrukcją return. Nie można ich wywołać wewnątrz instrukcji warunkowej, pętli ani funkcji zagnieżdżonych.
- Hooki muszą być wywoływane z funkcji React (wewnątrz komponentu React lub innego hooka). Nie powinno być wywoływane z funkcji Vanilla JS.
Hak useState
Hak useState
jest najbardziej podstawowym i użytecznym hakiem React. Podobnie jak inne wbudowane hooki, ten hook musi zostać zaimportowany z react
, aby mógł zostać użyty w naszej aplikacji.
import {useState} from 'react'
Aby zainicjować stan, musimy zadeklarować zarówno stan, jak i jego funkcję aktualizującą oraz przekazać wartość początkową.
const [state, updaterFn] = useState('')
Możemy dowolnie nazywać nasz stan i funkcję aktualizującą, ale zgodnie z konwencją, pierwszy element tablicy będzie naszym stanem, podczas gdy drugi element będzie funkcją aktualizującą. Powszechną praktyką jest poprzedzenie naszej funkcji aktualizującej zestawem przedrostków, po którym następuje nazwa naszego stanu w postaci wielkości wielbłąda.
Na przykład ustawmy stan do przechowywania wartości liczebności.
const [count, setCount] = useState(0)
Zauważ, że początkowa wartość naszego stanu count
jest ustawiona na 0
, a nie na pusty ciąg. Innymi słowy, możemy zainicjować nasz stan do dowolnego rodzaju zmiennych JavaScript, a mianowicie liczby, łańcucha, wartości logicznej, tablicy, obiektu, a nawet BigInt. Istnieje wyraźna różnica między stanami ustawień z hakiem useState
a stanami komponentów opartych na klasach. Warto zauważyć, że hook useState
zwraca tablicę, znaną również jako zmienne stanu, a w powyższym przykładzie zdestrukturyzowaliśmy tablicę do state
i funkcji updater
.
Renderowanie komponentów
Ustawienie stanów za pomocą haka useState
powoduje renderowanie odpowiedniego komponentu. Jednak dzieje się tak tylko wtedy, gdy React wykryje różnicę między poprzednim lub starym stanem a nowym stanem. React dokonuje porównania stanów za pomocą algorytmu Javascript Object.is
.
Ustawianie stanów za pomocą useState
Nasz stan count
można ustawić na nowe wartości stanu, po prostu przekazując nową wartość do funkcji aktualizującej setCount
w następujący sposób setCount(newValue)
.
Ta metoda działa, gdy nie chcemy odwoływać się do poprzedniej wartości stanu. Jeśli chcemy to zrobić, musimy przekazać funkcję do funkcji setCount
.
Zakładając, że chcemy dodać 5 do naszej zmiennej count
za każdym razem, gdy klikniemy przycisk, możemy wykonać następujące czynności.
import {useState} from 'react' const CountExample = () => { // initialize our count state const [count, setCount] = useState(0) // add 5 to to the count previous state const handleClick = () =>{ setCount(prevCount => prevCount + 5) } return( <div> <h1>{count} </h1> <button onClick={handleClick}>Add Five</button> </div> ) } export default CountExample
W powyższym kodzie najpierw zaimportowaliśmy hak useState
z react
, a następnie zainicjalizowaliśmy stan count
z domyślną wartością 0. Stworzyliśmy procedurę obsługi onClick
, która zwiększa wartość count
o 5 za każdym razem, gdy klikamy przycisk. Następnie wyświetlaliśmy wynik w tagu h1
.
Ustawianie tablic i stanów obiektów
Stany tablic i obiektów można ustawiać w taki sam sposób, jak inne typy danych. Jeśli jednak chcemy zachować już istniejące wartości, podczas ustawiania stanów musimy użyć operatora rozproszenia ES6.
Operator spread w Javascript jest używany do tworzenia nowego obiektu z już istniejącego obiektu. Jest to przydatne, ponieważ React
porównuje stany z operacją Object.is
, a następnie odpowiednio renderuje.
Rozważmy poniższy kod do ustawiania stanów po kliknięciu przycisku.
import {useState} from 'react' const StateExample = () => { //initialize our array and object states const [arr, setArr] = useState([2, 4]) const [obj, setObj] = useState({num: 1, name: 'Desmond'}) // set arr to the new array values const handleArrClick = () =>{ const newArr = [1, 5, 7] setArr([...arr, ...newArr]) } // set obj to the new object values const handleObjClick = () =>{ const newObj = {name: 'Ifeanyi', age: 25} setObj({...obj, ...newObj}) } return( <div> <button onClick ={handleArrClick}>Set Array State</button> <button onClick ={handleObjClick}>Set Object State</button> </div> ) } export default StateExample
W powyższym kodzie utworzyliśmy dwa stany arr
i obj
i zainicjalizowaliśmy je odpowiednio do niektórych wartości tablicy i obiektu. Następnie utworzyliśmy procedury obsługi onClick
o nazwie handleArrClick
i handleObjClick
, aby ustawić odpowiednio stany tablicy i obiektu. Po handleArrClick
funkcji handleArrClick wywołujemy setArr
i używamy operatora rozprzestrzeniania ES6, aby rozłożyć już istniejące wartości tablicy i dodać do niej newArr
.
To samo zrobiliśmy dla handleObjClick
. Tutaj nazwaliśmy setObj
, rozsunęliśmy istniejące wartości obiektów za pomocą operatora rozprzestrzeniania ES6 i zaktualizowaliśmy wartości name
i age
.
useState
charakter użytkowania Stan
Jak już widzieliśmy, stany ustawiamy za pomocą useState
, przekazując nową wartość do funkcji aktualizującej. Jeśli aktualizator zostanie wywołany wiele razy, nowe wartości zostaną dodane do kolejki, a ponowne renderowanie zostanie odpowiednio wykonane przy użyciu porównania JavaScript Object.is
.
Stany są aktualizowane asynchronicznie. Oznacza to, że nowy stan jest najpierw dodawany do stanu oczekiwania, a następnie stan jest aktualizowany. Tak więc nadal możesz uzyskać starą wartość stanu, jeśli uzyskasz dostęp do stanu natychmiast, gdy zostanie on ustawiony.
Rozważmy następujący przykład, aby zaobserwować to zachowanie.
W powyższym kodzie utworzyliśmy stan count
za pomocą haka useState
. Następnie utworzyliśmy procedurę obsługi onClick
, która zwiększa stan count
po każdym kliknięciu przycisku. Zauważ, że chociaż stan count
wzrósł, co jest wyświetlane w znaczniku h2
, poprzedni stan jest nadal rejestrowany w konsoli. Wynika to z asynchronicznej natury haka.
Jeśli chcemy uzyskać nowy stan, możemy obsłużyć go w podobny sposób, jakbyśmy obsługiwali funkcje asynchroniczne. Oto jeden ze sposobów, aby to zrobić.
Tutaj zapisaliśmy utworzoną newCountValue
do przechowywania zaktualizowanej wartości licznika, a następnie ustawiliśmy stan count
na zaktualizowaną wartość. Następnie zarejestrowaliśmy zaktualizowaną wartość licznika w konsoli.
Hak useEffect
useEffect
to kolejny ważny hak React używany w większości projektów. Działa podobnie do metod cyklu życia komponentu componentDidMount
, componentWillUnmount
i componentDidUpdate
. useEffect
daje nam możliwość pisania imperatywnych kodów, które mogą mieć skutki uboczne dla aplikacji. Przykłady takich efektów obejmują logowanie, subskrypcje, mutacje itp.
Użytkownik może zdecydować, kiedy useEffect
zostanie uruchomiony, jednak jeśli nie jest ustawiony, efekty uboczne będą się pojawiać przy każdym renderowaniu lub renderowaniu.
Rozważ poniższy przykład.
import {useState, useEffect} from 'react' const App = () =>{ const [count, setCount] = useState(0) useEffect(() =>{ console.log(count) }) return( <div> ... </div> ) }
W powyższym kodzie po prostu zarejestrowaliśmy count
w useEffect
. Uruchomi się to po każdym renderowaniu komponentu.
Czasami możemy chcieć uruchomić hook raz (na montowaniu) w naszym komponencie. Możemy to osiągnąć, podając drugi parametr do useEffect
.
import {useState, useEffect} from 'react' const App = () =>{ const [count, setCount] = useState(0) useEffect(() =>{ setCount(count + 1) }, []) return( <div> <h1>{count}</h1> ... </div> ) }
Hak useEffect
ma dwa parametry, pierwszy parametr to funkcja, którą chcemy uruchomić, a drugi to tablica zależności. Jeśli drugi parametr nie zostanie podany, podpięcie będzie działać w sposób ciągły.
Przekazując pusty nawias kwadratowy do drugiego parametru haka, nakazujemy Reactowi uruchomienie haka useEffect
tylko raz, na montowaniu. Spowoduje to wyświetlenie wartości 1
w znaczniku h1
, ponieważ licznik zostanie zaktualizowany raz, od 0 do 1, po zamontowaniu komponentu.
Moglibyśmy również sprawić, że nasz efekt uboczny będzie działał, gdy zmienią się niektóre wartości zależne. Można to zrobić, przekazując te wartości na liście zależności.
Na przykład, możemy sprawić, że useEffect
będzie uruchamiany za każdym razem, gdy count
zmiany w następujący sposób.
import { useState, useEffect } from "react"; const App = () => { const [count, setCount] = useState(0); useEffect(() => { console.log(count); }, [count]); return ( <div> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default App;
Powyższy useEffect
zostanie uruchomiony, gdy spełniony zostanie jeden z tych dwóch warunków.
- Po zamontowaniu — po wyrenderowaniu komponentu.
- Gdy zmienia się wartość
count
.
Podczas montowania zostanie uruchomione wyrażenie console.log
i count
dzienników będzie wynosić 0. Po zaktualizowaniu count
spełniony jest drugi warunek, więc funkcja useEffect
uruchomiona ponownie i będzie kontynuowana za każdym razem, gdy przycisk zostanie kliknięty.
Po podaniu drugiego argumentu useEffect
oczekuje się, że przekażemy do niego wszystkie zależności. Jeśli masz zainstalowany ESLINT
, wyświetli błąd lint, jeśli jakakolwiek zależność nie zostanie przekazana do listy parametrów. Może to również spowodować nieoczekiwane zachowanie efektu ubocznego, zwłaszcza jeśli zależy to od parametrów, które nie są przekazywane.
Czyszczenie efektu
useEffect
pozwala nam również wyczyścić zasoby przed odmontowaniem komponentu. Może to być konieczne, aby zapobiec wyciekom pamięci i zwiększyć wydajność aplikacji. Aby to zrobić, zwrócilibyśmy funkcję czyszczenia na końcu haka.
useEffect(() => { console.log('mounted') return () => console.log('unmounting... clean up here') })
Powyższy haczyk useEffect
będzie rejestrował mounted
po zamontowaniu komponentu. Odmontowywanie… czyszczenie tutaj zostanie zarejestrowane po odmontowaniu komponentu. Może się to zdarzyć, gdy składnik zostanie usunięty z interfejsu użytkownika.
Proces czyszczenia zazwyczaj przebiega zgodnie z poniższym formularzem.
useEffect(() => { //The effect we intend to make effect //We then return the clean up return () => the cleanup/unsubscription })
Chociaż możesz nie znaleźć tak wielu przypadków użycia subskrypcji useEffect
, jest to przydatne w przypadku subskrypcji i liczników czasu. W szczególności w przypadku gniazd sieciowych może być konieczne wypisanie się z sieci, aby zaoszczędzić zasoby i poprawić wydajność po odmontowaniu komponentu.
Pobieranie i ponowne pobieranie danych za pomocą useEffect
Jednym z najczęstszych przypadków użycia haka useEffect
jest pobieranie i pobieranie danych z API.
Aby to zilustrować, użyjemy fałszywych danych użytkownika utworzonych przeze mnie z JSONPlaceholder
do pobrania danych za pomocą haka useEffect
.
import { useEffect, useState } from "react"; import axios from "axios"; export default function App() { const [users, setUsers] = useState([]); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/users"; useEffect(() => { const fetchUsers = async () => { const { data } = await axios.get(endPoint); setUsers(data); }; fetchUsers(); }, []); return ( <div className="App"> {users.map((user) => ( <div> <h2>{user.name}</h2> <p>Occupation: {user.job}</p> <p>Sex: {user.sex}</p> </div> ))} </div> ); }
W powyższym kodzie stworzyliśmy stan users
za pomocą useState
. Następnie pobraliśmy dane z API za pomocą Axios. Jest to proces asynchroniczny, więc użyliśmy funkcji async/await, mogliśmy również użyć kropki, a następnie składni. Ponieważ pobraliśmy listę użytkowników, po prostu zmapowaliśmy ją, aby wyświetlić dane.
Zauważ, że przekazaliśmy do haka pusty parametr. Gwarantuje to, że zostanie wywołany tylko raz, gdy komponent zostanie zamontowany.
Możemy również ponownie pobrać dane, gdy zmienią się niektóre warunki. Pokażemy to w poniższym kodzie.
import { useEffect, useState } from "react"; import axios from "axios"; export default function App() { const [userIDs, setUserIDs] = useState([]); const [user, setUser] = useState({}); const [currentID, setCurrentID] = useState(1); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/userdata/users"; useEffect(() => { axios.get(endPoint).then(({ data }) => setUserIDs(data)); }, []); useEffect(() => { const fetchUserIDs = async () => { const { data } = await axios.get(`${endPoint}/${currentID}`}); setUser(data); }; fetchUserIDs(); }, [currentID]); const moveToNextUser = () => { setCurrentID((prevId) => (prevId < userIDs.length ? prevId + 1 : prevId)); }; const moveToPrevUser = () => { setCurrentID((prevId) => (prevId === 1 ? prevId : prevId - 1)); }; return ( <div className="App"> <div> <h2>{user.name}</h2> <p>Occupation: {user.job}</p> <p>Sex: {user.sex}</p> </div> <button onClick={moveToPrevUser}>Prev</button> <button onClick={moveToNextUser}>Next</button> </div> ); }
Tutaj stworzyliśmy dwa haki useEffect
. W pierwszym użyliśmy składni kropka, a następnie, aby pobrać wszystkich użytkowników z naszego API. Jest to konieczne do określenia liczby użytkowników.
Następnie utworzyliśmy kolejny haczyk useEffect
, aby pobrać użytkownika na podstawie id
. Ten useEffect
ponownie pobierze dane za każdym razem, gdy zmieni się identyfikator. Aby to zapewnić, przekazaliśmy id
na liście zależności.
Następnie stworzyliśmy funkcje aktualizujące wartość naszego id
za każdym razem, gdy klikamy przyciski. Po zmianie wartości id
useEffect
uruchomi się ponownie i pobierze dane.
Jeśli chcemy, możemy nawet wyczyścić lub anulować token oparty na obietnicy w Axios, możemy to zrobić za pomocą metody czyszczenia omówionej powyżej.
useEffect(() => { const source = axios.CancelToken.source(); const fetchUsers = async () => { const { data } = await axios.get(`${endPoint}/${num}`, { cancelToken: source.token }); setUser(data); }; fetchUsers(); return () => source.cancel(); }, [num]);
Tutaj przekazaliśmy token Axios jako drugi parametr do axios.get
. Po odmontowaniu komponentu anulowaliśmy subskrypcję, wywołując metodę cancel obiektu źródłowego.
Hak useReducer
Hak useReducer
jest bardzo przydatnym hakiem React, który działa podobnie do haczyka useState
. Zgodnie z dokumentacją Reacta, ten hak powinien być używany do obsługi bardziej złożonej logiki niż hak useState
. Warto zauważyć, że hak useState
jest wewnętrznie zaimplementowany z hakiem useReducer.
Zaczep przyjmuje reduktor jako argument i opcjonalnie może przyjąć stan początkowy i funkcję init jako argumenty.
const [state, dispatch] = useReducer(reducer, initialState, init)
Tutaj init
jest funkcją i jest używany zawsze, gdy chcemy leniwie utworzyć stan początkowy.
Zobaczmy, jak zaimplementować hak useReducer
, tworząc prostą aplikację do zrobienia, jak pokazano w piaskownicy poniżej.
Po pierwsze, powinniśmy stworzyć nasz reduktor do utrzymania stanów.
export const ADD_TODO = "ADD_TODO"; export const REMOVE_TODO = "REMOVE_TODO"; export const COMPLETE_TODO = "COMPLETE_TODO"; const reducer = (state, action) => { switch (action.type) { case ADD_TODO: const newTodo = { id: action.id, text: action.text, completed: false }; return [...state, newTodo]; case REMOVE_TODO: return state.filter((todo) => todo.id !== action.id); case COMPLETE_TODO: const completeTodo = state.map((todo) => { if (todo.id === action.id) { return { ...todo, completed: !todo.completed }; } else { return todo; } }); return completeTodo; default: return state; } }; export default reducer;
Stworzyliśmy trzy stałe odpowiadające naszym typom akcji. Mogliśmy użyć łańcuchów bezpośrednio, ale ta metoda jest lepsza, aby uniknąć literówek.
Następnie stworzyliśmy naszą funkcję reduktora. Podobnie jak w Redux
, reduktor musi przyjąć stan i obiekt akcji. Ale w przeciwieństwie do Redux, nie musimy tutaj inicjować naszego reduktora.
Co więcej, w wielu przypadkach użycia związanych z zarządzaniem stanem, useReducer
wraz z dispatch
ujawnionym za pośrednictwem kontekstu może umożliwić większej aplikacji uruchamianie akcji, aktualizowanie state
i nasłuchiwanie go.
Następnie użyliśmy instrukcji switch
do sprawdzenia typu akcji przekazanej przez użytkownika. Jeśli typem akcji jest ADD_TODO
, chcemy przekazać nowe zadanie , a jeśli jest to REMOVE_TODO
, chcemy przefiltrować zadania i usunąć to, które odpowiada id
przekazanemu przez użytkownika. Jeśli jest to COMPLETE_TODO
, chcemy zmapować zadania i przełączyć to z id
przekazanym przez użytkownika.
Oto plik App.js
, w którym zaimplementowaliśmy reducer
.
import { useReducer, useState } from "react"; import "./styles.css"; import reducer, { ADD_TODO, REMOVE_TODO, COMPLETE_TODO } from "./reducer"; export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = [ { id: id, text: "First Item", completed: false } ]; //We could also pass an empty array as the initial state //const initialState = [] const [state, dispatch] = useReducer(reducer, initialState); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); dispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; const removeTodo = (id) => { dispatch({ type: REMOVE_TODO, id }); }; const completeTodo = (id) => { dispatch({ type: COMPLETE_TODO, id }); }; return ( <div className="App"> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit">+</button> </form> <div className="todos"> {state.map((todo) => ( <div key={todo.id} className="todoItem"> <p className={todo.completed && "strikethrough"}>{todo.text}</p> <span onClick={() => removeTodo(todo.id)}>✕</span> <span onClick={() => completeTodo(todo.id)}>✓</span> </div> ))} </div> </div> ); }
Tutaj stworzyliśmy formularz zawierający element input do zbierania danych wejściowych użytkownika oraz przycisk wyzwalający akcję. Po przesłaniu formularza wywołaliśmy akcję typu ADD_TODO
, przekazując nowy identyfikator i tekst zadania. Utworzyliśmy nowy identyfikator, zwiększając poprzednią wartość identyfikatora o 1. Następnie wyczyściliśmy pole tekstowe wprowadzania. Aby usunąć i zakończyć zadanie, po prostu wykonaliśmy odpowiednie działania. Zostały one już zaimplementowane w reduktorze, jak pokazano powyżej.
Jednak magia dzieje się, ponieważ używamy haka useReducer
. Ten hook akceptuje reduktor i stan początkowy oraz zwraca stan i funkcję wysyłania. W tym przypadku funkcja dispatch służy do tego samego celu co funkcja setter dla useState
i możemy ją nazwać jak tylko chcemy zamiast dispatch
.
Aby wyświetlić czynności do wykonania, po prostu zmapowaliśmy listę czynności do wykonania zwróconych w naszym obiekcie stanu, jak pokazano w powyższym kodzie.
To pokazuje moc haka useReducer
. Moglibyśmy również osiągnąć tę funkcjonalność za pomocą haka useState
, ale jak widać z powyższego przykładu, hak useReducer
pomógł nam zachować porządek. useReducer
jest często korzystny, gdy obiekt stanu jest złożoną strukturą i jest aktualizowany na różne sposoby w przeciwieństwie do prostej wartości zastępczej. Ponadto, gdy te funkcje aktualizacji stają się bardziej skomplikowane, useReducer
ułatwia przechowywanie całej tej złożoności w funkcji redukującej (która jest czystą funkcją JS), co bardzo ułatwia pisanie testów dla samej funkcji redukującej.
Mogliśmy również przekazać trzeci argument do haka useReducer
, aby leniwie utworzyć stan początkowy. Oznacza to, że możemy obliczyć stan początkowy w funkcji init
.
Na przykład możemy stworzyć funkcję init
w następujący sposób:
const initFunc = () => [ { id: id, text: "First Item", completed: false } ]
a następnie przekaż go do naszego haka useReducer
.
const [state, dispatch] = useReducer(reducer, initialState, initFunc)
Jeśli to zrobimy, funkcja initFunc
nadpisze initialState
przez nas stan początkowy, a stan początkowy zostanie obliczony leniwie.
Hook useContext
Interfejs API React Context umożliwia udostępnianie stanów lub danych w całym drzewie komponentów React. API było dostępne w React jako funkcja eksperymentalna od jakiegoś czasu, ale stało się bezpieczne w użyciu w React 16.3.0. Interfejs API ułatwia udostępnianie danych między komponentami, eliminując jednocześnie wiercenie pod śruby.
Chociaż możesz zastosować kontekst reakcji do całej aplikacji, możliwe jest również zastosowanie go do części aplikacji.
Aby użyć zaczepu, musisz najpierw utworzyć kontekst za pomocą React.createContext
, a następnie ten kontekst można przekazać do zaczepu.
Aby zademonstrować użycie useContext
, stwórzmy prostą aplikację, która zwiększy rozmiar czcionki w całej naszej aplikacji.
Stwórzmy nasz kontekst w pliku context.js
.
import { createContext } from "react"; //Here, we set the initial fontSize as 16. const fontSizeContext = createContext(16); export default fontSizeContext;
Tutaj utworzyliśmy kontekst i przekazaliśmy do niego początkową wartość 16
, a następnie wyeksportowaliśmy kontekst. Następnie połączmy nasz kontekst z naszą aplikacją.
import FontSizeContext from "./context"; import { useState } from "react"; import PageOne from "./PageOne"; import PageTwo from "./PageTwo"; const App = () => { const [size, setSize] = useState(16); return ( <FontSizeContext.Provider value={size}> <PageOne /> <PageTwo /> <button onClick={() => setSize(size + 5)}>Increase font</button> <button onClick={() => setSize((prevSize) => Math.min(11, prevSize - 5)) } > Decrease font </button> </FontSizeContext.Provider> ); }; export default App;
W powyższym kodzie owinęliśmy całe nasze drzewo komponentów za pomocą FontSizeContext.Provider
i przekazaliśmy size
do jego właściwości wartości. Tutaj size
jest stanem utworzonym za pomocą useState
. To pozwala nam zmieniać wartość właściwości za każdym razem, gdy zmienia się stan size
. Zawijając cały komponent z Provider
, możemy uzyskać dostęp do kontekstu w dowolnym miejscu naszej aplikacji.
Na przykład uzyskaliśmy dostęp do kontekstu w <PageOne />
i <PageTwo />
. W rezultacie rozmiar czcionki wzrośnie w tych dwóch składnikach, gdy zwiększymy go z pliku App.js
Możemy zwiększyć lub zmniejszyć rozmiar czcionki za pomocą przycisków, jak pokazano powyżej, a gdy to zrobimy, rozmiar czcionki zmieni się w całej aplikacji.
import { useContext } from "react"; import context from "./context"; const PageOne = () => { const size = useContext(context); return <p style={{ fontSize: `${size}px` }}>Content from the first page</p>; }; export default PageOne;
Tutaj uzyskaliśmy dostęp do kontekstu za pomocą haka useContext
z naszego komponentu PageOne
. Następnie użyliśmy tego kontekstu do ustawienia naszej właściwości font-size. Podobna procedura dotyczy pliku PageTwo.js
.
Motywy lub inne konfiguracje wyższego rzędu na poziomie aplikacji są dobrymi kandydatami na konteksty.
Korzystanie useContext
i useReducer
W przypadku użycia z hakiem useReducer
, useContext
pozwala nam stworzyć własny system zarządzania stanem. Możemy tworzyć globalne stany i łatwo nimi zarządzać w naszej aplikacji.
Ulepszmy naszą aplikację do zrobienia za pomocą kontekstowego API.
Jak zwykle musimy utworzyć todoContext
w pliku todoContext.js
.
import { createContext } from "react"; const initialState = []; export default createContext(initialState);
Tutaj stworzyliśmy kontekst, przekazując początkową wartość pustej tablicy. Następnie wyeksportowaliśmy kontekst.
App.js
nasz plik App.js, oddzielając listę rzeczy do zrobienia i elementy.
import { useReducer, useState } from "react"; import "./styles.css"; import todoReducer, { ADD_TODO } from "./todoReducer"; import TodoContext from "./todoContext"; import TodoList from "./TodoList"; export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = []; const [todoState, todoDispatch] = useReducer(todoReducer, initialState); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); todoDispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; return ( <TodoContext.Provider value={[todoState, todoDispatch]}> <div className="app"> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit"> + </button> </form> <TodoList /> </div> </TodoContext.Provider> ); }
Tutaj opakowaliśmy nasz plik App.js
z TodoContext.Provider
, a następnie przekazaliśmy do niego wartości zwracane przez nasz todoReducer
. Dzięki temu stan reduktora i funkcja dispatch
będą dostępne w całej naszej aplikacji.
Następnie podzieliliśmy ekran rzeczy do zrobienia na komponent TodoList
. Zrobiliśmy to bez drążenia rekwizytów, dzięki Context API. Przyjrzyjmy się plikowi TodoList.js
.
import React, { useContext } from "react"; import TodoContext from "./todoContext"; import Todo from "./Todo"; const TodoList = () => { const [state] = useContext(TodoContext); return ( <div className="todos"> {state.map((todo) => ( <Todo key={todo.id} todo={todo} /> ))} </div> ); }; export default TodoList;
Korzystając z destrukturyzacji tablicy, możemy uzyskać dostęp do stanu (pozostawiając funkcję dispatch) z kontekstu za pomocą useContext
. Następnie możemy mapować stan i wyświetlać elementy do wykonania. Nadal wyodrębniliśmy to w komponencie Todo
. Funkcja mapy ES6+ wymaga od nas podania unikalnego klucza, a ponieważ potrzebujemy konkretnego zadania, przekazujemy go również obok.
Rzućmy okiem na komponent Todo
.
import React, { useContext } from "react"; import TodoContext from "./todoContext"; import { REMOVE_TODO, COMPLETE_TODO } from "./todoReducer"; const Todo = ({ todo }) => { const [, dispatch] = useContext(TodoContext); const removeTodo = (id) => { dispatch({ type: REMOVE_TODO, id }); }; const completeTodo = (id) => { dispatch({ type: COMPLETE_TODO, id }); }; return ( <div className="todoItem"> <p className={todo.completed ? "strikethrough" : "nostrikes"}> {todo.text} </p> <span onClick={() => removeTodo(todo.id)}>✕</span> <span onClick={() => completeTodo(todo.id)}>✓</span> </div> ); }; export default Todo;
Ponownie używając destrukturyzacji tablicy, uzyskaliśmy dostęp do funkcji wysyłania z kontekstu. To pozwala nam zdefiniować funkcje completeTodo
i removeTodo
, jak już omówiono w sekcji useReducer
. Dzięki właściwości todoList.js
todo
wyświetlić pozycję do zrobienia. Możemy również oznaczyć to jako ukończone i usunąć zadanie według własnego uznania.
Możliwe jest również zagnieżdżenie więcej niż jednego dostawcy kontekstu w katalogu głównym naszej aplikacji. Oznacza to, że możemy używać więcej niż jednego kontekstu do wykonywania różnych funkcji w aplikacji.
Aby to zademonstrować, dodajmy motywy do przykładu rzeczy do zrobienia.
Oto, co będziemy budować.
Ponownie musimy stworzyć themeContext
. Aby to zrobić, utwórz plik themeContext.js
i dodaj następujące kody.
import { createContext } from "react"; import colors from "./colors"; export default createContext(colors.light);
Tutaj stworzyliśmy kontekst i przekazaliśmy colors.light
jako wartość początkową. Zdefiniujmy kolory za pomocą tej właściwości w pliku colors.js
.
const colors = { light: { backgroundColor: "#fff", color: "#000" }, dark: { backgroundColor: "#000", color: "#fff" } }; export default colors;
W powyższym kodzie stworzyliśmy obiekt colors
zawierający właściwości jasne i ciemne. Każda właściwość ma obiekt backgroundColor
i color
.
Następnie tworzymy themeReducer
do obsługi stanów motywu.
import Colors from "./colors"; export const LIGHT = "LIGHT"; export const DARK = "DARK"; const themeReducer = (state, action) => { switch (action.type) { case LIGHT: return { ...Colors.light }; case DARK: return { ...Colors.dark }; default: return state; } }; export default themeReducer;
Podobnie jak wszystkie reduktory, themeReducer
przyjmuje stan i akcję. Następnie używa instrukcji switch
do określenia bieżącej akcji. Jeśli jest typu LIGHT
, po prostu przypisujemy Colors.light
, a jeśli jest typu DARK
, wyświetlamy Colors.dark
. Moglibyśmy to łatwo zrobić za pomocą haka useState
, ale wybieramy useReducer
, aby skierować punkt do domu.
Po skonfigurowaniu themeReducer
możemy zintegrować go z naszym plikiem App.js
import { useReducer, useState, useCallback } from "react"; import "./styles.css"; import todoReducer, { ADD_TODO } from "./todoReducer"; import TodoContext from "./todoContext"; import ThemeContext from "./themeContext"; import TodoList from "./TodoList"; import themeReducer, { DARK, LIGHT } from "./themeReducer"; import Colors from "./colors"; import ThemeToggler from "./ThemeToggler"; const themeSetter = useCallback( theme => themeDispatch({type: theme}, [themeDispatch]); export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = []; const [todoState, todoDispatch] = useReducer(todoReducer, initialState); const [themeState, themeDispatch] = useReducer(themeReducer, Colors.light); const themeSetter = useCallback( (theme) => { themeDispatch({ type: theme }); }, [themeDispatch] ); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); todoDispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; return ( <TodoContext.Provider value={[todoState, todoDispatch]}> <ThemeContext.Provider value={[ themeState, themeSetter ]} > <div className="app" style={{ ...themeState }}> <ThemeToggler /> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit"> + </button> </form> <TodoList /> </div> </ThemeContext.Provider> </TodoContext.Provider> ); }
W powyższym kodzie dodaliśmy kilka rzeczy do naszej już istniejącej aplikacji do zrobienia. Zaczęliśmy od zaimportowania ThemeContext
, themeReducer
, ThemeToggler
i Colors
. We created a reducer using the useReducer
hook, passing the themeReducer
and an initial value of Colors.light
to it. This returned the themeState
and themeDispatch
to us.
We then nested our component with the provider function from the ThemeContext
, passing the themeState
and the dispatch
functions to it. We also added theme styles to it by spreading out the themeStates
. This works because the colors
object already defined properties similar to what the JSX styles will accept.
However, the actual theme toggling happens in the ThemeToggler
component. Przyjrzyjmy się temu.
import ThemeContext from "./themeContext"; import { useContext, useState } from "react"; import { DARK, LIGHT } from "./themeReducer"; const ThemeToggler = () => { const [showLight, setShowLight] = useState(true); const [themeState, themeSetter] = useContext(ThemeContext); const dispatchDarkTheme = () => themeSetter(DARK); const dispatchLightTheme = () => themeSetter(LIGHT); const toggleTheme = () => { showLight ? dispatchDarkTheme() : dispatchLightTheme(); setShowLight(!showLight); }; console.log(themeState); return ( <div> <button onClick={toggleTheme}> {showLight ? "Change to Dark Theme" : "Change to Light Theme"} </button> </div> ); }; export default ThemeToggler;
In this component, we used the useContext
hook to retrieve the values we passed to the ThemeContext.Provider
from our App.js
file. As shown above, these values include the ThemeState
, dispatch function for the light theme, and dispatch function for the dark theme. Thereafter, we simply called the dispatch functions to toggle the themes. We also created a state showLight
to determine the current theme. This allows us to easily change the button text depending on the current theme.
The useMemo
Hook
The useMemo
hook is designed to memoize expensive computations. Memoization simply means caching. It caches the computation result with respect to the dependency values so that when the same values are passed, useMemo
will just spit out the already computed value without recomputing it again. This can significantly improve performance when done correctly.
The hook can be used as follows:
const memoizedResult = useMemo(() => expensiveComputation(a, b), [a, b])
Let's consider three cases of the useMemo
hook.
- When the dependency values, a and b remain the same.
TheuseMemo
hook will return the already computed memoized value without recomputation. - When the dependency values, a and b change.
The hook will recompute the value. - When no dependency value is passed.
The hook will recompute the value.
Let's take a look at an example to demonstrate this concept.
In the example below, we'll be computing the PAYE and Income after PAYE of a company's employees with fake data from JSONPlaceholder.
The calculation will be based on the personal income tax calculation procedure for Nigeria providers by PricewaterhouseCoopers available here.
This is shown in the sandbox below.
First, we queried the API to get the employees' data. We also get data for each employee (with respect to their employee id).
const [employee, setEmployee] = useState({}); const [employees, setEmployees] = useState([]); const [num, setNum] = useState(1); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/employees"; useEffect(() => { const getEmployee = async () => { const { data } = await axios.get(`${endPoint}/${num}`); setEmployee(data); }; getEmployee(); }, [num]); useEffect(() => { axios.get(endPoint).then(({ data }) => setEmployees(data)); }, [num]);
Użyliśmy axios
i metody async/await
await w pierwszym useEffect
, a następnie kropki i składni w drugim. Te dwa podejścia działają w ten sam sposób.
Następnie, korzystając z danych pracowników, które otrzymaliśmy z góry, obliczmy zmienne ulgi:
const taxVariablesCompute = useMemo(() => { const { income, noOfChildren, noOfDependentRelatives } = employee; //supposedly complex calculation //tax relief computations for relief Allowance, children relief, // relatives relief and pension relief const reliefs = reliefAllowance1 + reliefAllowance2 + childrenRelief + relativesRelief + pensionRelief; return reliefs; }, [employee]);
Jest to dość złożone obliczenie, więc musieliśmy owinąć je w hook useMemo
, aby je zapamiętać lub zoptymalizować. Zapamiętywanie go w ten sposób zapewni, że kalkulacja nie zostanie ponownie obliczona, jeśli ponownie spróbujemy uzyskać dostęp do tego samego pracownika.
Ponadto, korzystając z uzyskanych wartości ulg podatkowych, chcielibyśmy obliczyć PAYE i dochód po PAYE.
const taxCalculation = useMemo(() => { const { income } = employee; let taxableIncome = income - taxVariablesCompute; let PAYE = 0; //supposedly complex calculation //computation to compute the PAYE based on the taxable income and tax endpoints const netIncome = income - PAYE; return { PAYE, netIncome }; }, [employee, taxVariablesCompute]);
Przeprowadziliśmy obliczenie podatku (dość skomplikowane obliczenie) za pomocą wyżej obliczonych zmiennych podatkowych, a następnie zapamiętaliśmy je za pomocą useMemo
.
Pełny kod dostępny jest tutaj.
Jest to zgodne z procedurą obliczania podatku podaną tutaj. Najpierw obliczyliśmy ulgę podatkową, biorąc pod uwagę dochód, liczbę dzieci i liczbę członków rodziny pozostających na utrzymaniu. Następnie stopniowo przemnożyliśmy dochód do opodatkowania przez stawki PIT. Chociaż obliczenia, o których mowa, nie są całkowicie konieczne w tym samouczku, mają one na celu pokazanie nam, dlaczego useMemo
może być konieczne. Jest to również dość skomplikowana kalkulacja, więc może być konieczne zapamiętanie jej za pomocą useMemo
, jak pokazano powyżej.
Po obliczeniu wartości wyświetlaliśmy po prostu wynik.
Zwróć uwagę na następujące informacje o haczyku useMemo
.
-
useMemo
należy używać tylko wtedy, gdy jest to konieczne do optymalizacji obliczeń. Innymi słowy, gdy przeliczenie jest kosztowne. - Zaleca się, aby najpierw zapisać obliczenia bez zapamiętywania i zapamiętywać je tylko wtedy, gdy powoduje to problemy z wydajnością.
- Niepotrzebne i nieistotne użycie haka
useMemo
może nawet pogorszyć problemy z wydajnością. - Czasami zbyt duża ilość zapamiętywania może również powodować problemy z wydajnością.
Zastosowanie Haka useCallback
useCallback
służy temu samemu celowi co useMemo
, ale zwraca zapamiętane wywołanie zwrotne zamiast zapamiętanej wartości. Innymi słowy, useCallback
jest tym samym, co przekazywanie useMemo
bez wywołania funkcji.
Rozważmy na przykład poniższe kody.
import React, {useCallback, useMemo} from 'react' const MemoizationExample = () => { const a = 5 const b = 7 const memoResult = useMemo(() => a + b, [a, b]) const callbackResult = useCallback(a + b, [a, b]) console.log(memoResult) console.log(callbackResult) return( <div> ... </div> ) } export default MemoizationExample
W powyższym przykładzie zarówno memoResult
, jak i callbackResult
dadzą tę samą wartość 12
. Tutaj useCallback
zwróci zapamiętaną wartość. Jednak możemy również sprawić, by zwracał zapamiętane wywołanie zwrotne, przekazując je jako funkcję.
Poniższe useCallback
zwróci zapamiętane wywołanie zwrotne.
... const callbackResult = useCallback(() => a + b, [a, b]) ...
Następnie możemy wywołać wywołanie zwrotne, gdy akcja jest wykonywana lub w haczyku useEffect
.
import {useCallback, useEffect} from 'react' const memoizationExample = () => { const a = 5 const b = 7 const callbackResult = useCallback(() => a + b, [a, b]) useEffect(() => { const callback = callbackResult() console.log(callback) }) return ( <div> <button onClick= {() => console.log(callbackResult())}> Trigger Callback </button> </div> ) } export default memoizationExample
W powyższym kodzie zdefiniowaliśmy funkcję wywołania zwrotnego za pomocą haka useCallback
. Następnie wywołaliśmy wywołanie zwrotne w haczyku useEffect
, gdy komponent zostanie zamontowany, a także po kliknięciu przycisku.
Zarówno useEffect
, jak i kliknięcie przycisku dają ten sam wynik.
Zwróć uwagę, że pojęcia, zalecenia i zakazy, które dotyczą haka useMemo
, odnoszą się również do haka useCallback
. Możemy odtworzyć przykład useMemo
za pomocą useCallback
.
Hak useRef
useRef
zwraca obiekt, który może trwać w aplikacji. Hak ma tylko jedną właściwość, current
i możemy łatwo przekazać do niego argument.
Służy temu samemu celowi co createRef
używane w komponentach opartych na klasach. Możemy utworzyć referencję za pomocą tego haka w następujący sposób:
const newRef = useRef('')
Tutaj stworzyliśmy nowy ref o nazwie newRef
i przekazaliśmy do niego pusty ciąg.
Ten haczyk służy głównie do dwóch celów:
- Uzyskiwanie dostępu do DOM lub manipulowanie nim oraz
- Przechowywanie stanów zmiennych — jest to przydatne, gdy nie chcemy, aby komponent był renderowany po zmianie wartości.
Manipulowanie DOM
Po przekazaniu do elementu DOM obiekt ref wskazuje na ten element i może być użyty do uzyskania dostępu do jego atrybutów i właściwości DOM.
Oto bardzo prosty przykład demonstrujący tę koncepcję.
import React, {useRef, useEffect} from 'react' const RefExample = () => { const headingRef = useRef('') console.log(headingRef) return( <div> <h1 className='topheading' ref={headingRef}>This is a h1 element</h1> </div> ) } export default RefExample
W powyższym przykładzie zdefiniowaliśmy nagłówek nagłówka używając useRef
headingRef
pusty ciąg. Następnie ustawiamy ref w znaczniku h1
, przekazując ref = {headingRef}
. Ustawiając ten ref, poprosiliśmy headingRef
, aby wskazywał na nasz element h1
. Oznacza to, że możemy uzyskać dostęp do właściwości naszego elementu h1
z ref.
Aby to zobaczyć, jeśli sprawdzimy wartość console.log(headingRef)
, otrzymamy {current: HTMLHeadingElement}
lub {current: h1}
i możemy ocenić wszystkie właściwości lub atrybuty elementu. Podobna rzecz dotyczy każdego innego elementu HTML.
Na przykład, możemy ustawić kursywę podczas montowania komponentu.
useEffect(() => { headingRef.current.style.font; }, []);
Możemy nawet zmienić tekst na inny.
... headingRef.current.innerHTML = "A Changed H1 Element"; ...
Możemy nawet zmienić kolor tła kontenera nadrzędnego.
... headingRef.current.parentNode.style.backgroundColor = "red"; ...
Tutaj można dokonać dowolnego rodzaju manipulacji DOM. Zauważ, że headingRef.current
można odczytać w taki sam sposób jak document.querySelector('.topheading')
.
Jednym z interesujących przypadków użycia haka useRef
w manipulowaniu elementem DOM jest skupienie kursora na elemencie wejściowym. Przeanalizujmy to szybko.
import {useRef, useEffect} from 'react' const inputRefExample = () => { const inputRef = useRef(null) useEffect(() => { inputRef.current.focus() }, []) return( <div> <input ref={inputRef} /> <button onClick = {() => inputRef.current.focus()}>Focus on Input </button> </div> ) } export default inputRefExample
W powyższym kodzie stworzyliśmy inputRef
za pomocą useRef
, a następnie poprosiliśmy go o wskazanie elementu input. Następnie ustawiliśmy kursor na odnośniku wejściowym po załadowaniu komponentu i kliknięciu przycisku za pomocą inputRef.current.focus()
. Jest to możliwe, ponieważ focus()
jest atrybutem elementów wejściowych, więc ref będzie mógł ocenić metody.
Referencje utworzone w komponencie nadrzędnym mogą być ocenione w komponencie potomnym poprzez przekazanie go za pomocą React.forwardRef()
. Przyjrzyjmy się temu.
Stwórzmy najpierw kolejny komponent NewInput.js
i dodajmy do niego poniższe kody.
import { useRef, forwardRef } from "react"; const NewInput = forwardRef((props, ref) => { return <input placeholder={props.val} ref={ref} />; }); export default NewInput;
Ten komponent akceptuje props
i ref
. Przekazaliśmy ref do jego właściwości ref, a props.val
do jego właściwości zastępczej. Zwykłe komponenty React nie przyjmują atrybutu ref
. Ten atrybut jest dostępny tylko wtedy, gdy owiniemy go React.forwardRef
, jak pokazano powyżej.
Możemy wtedy łatwo nazwać to w komponencie nadrzędnym.
... <NewInput val="Just an example" ref={inputRef} /> ...
Przechowywanie stanów zmiennych
Odniesienia służą nie tylko do manipulowania elementami DOM, ale mogą być również używane do przechowywania zmiennych wartości bez ponownego renderowania całego komponentu.
Poniższy przykład wykryje, ile razy przycisk został kliknięty bez ponownego renderowania komponentu.
import { useRef } from "react"; export default function App() { const countRef = useRef(0); const increment = () => { countRef.current++; console.log(countRef); }; return ( <div className="App"> <button onClick={increment}>Increment </button> </div> ); }
W powyższym kodzie zwiększyliśmy countRef
po kliknięciu przycisku, a następnie zarejestrowaliśmy go w konsoli. Chociaż wartość jest zwiększana, jak pokazano w konsoli, nie będziemy w stanie zobaczyć żadnych zmian, jeśli spróbujemy ocenić ją bezpośrednio w naszym komponencie. Zaktualizuje się tylko w komponencie, gdy zostanie ponownie wyrenderowany.
Zauważ, że podczas gdy useState
jest asynchroniczne, useRef
jest synchroniczne. Innymi słowy, wartość jest dostępna natychmiast po jej aktualizacji.
Hak useLayoutEffect
Podobnie jak hak useEffect
, useLayoutEffect
jest wywoływane po zamontowaniu i wyrenderowaniu komponentu. Ten hak odpala po mutacji DOM i robi to synchronicznie. Oprócz wywoływania synchronicznego po mutacji DOM, useLayoutEffect
robi to samo, co useEffect
.
useLayoutEffect
powinien być używany tylko do wykonywania mutacji DOM lub pomiarów związanych z DOM, w przeciwnym razie należy użyć haka useEffect
. Używanie haka useEffect
dla funkcji mutacji DOM może powodować pewne problemy z wydajnością, takie jak migotanie, ale useLayoutEffect
radzi sobie z nimi doskonale, ponieważ działa po wystąpieniu mutacji.
Rzućmy okiem na kilka przykładów, aby zademonstrować tę koncepcję.
- Przy zmianie rozmiaru uzyskamy szerokość i wysokość okna.
import {useState, useLayoutEffect} from 'react' const ResizeExample = () =>{ const [windowSize, setWindowSize] = useState({width: 0, height: 0}) useLayoutEffect(() => { const resizeWindow = () => setWindowSize({ width: window.innerWidth, height: window.innerHeight }) window.addEventListener('resize', resizeWindow) return () => window.removeEventListener('resize', resizeWindow) }, []) return ( <div> <p>width: {windowSize.width}</p> <p>height: {windowSize.height}</p> </div> ) } export default ResizeExample
W powyższym kodzie stworzyliśmy stan windowSize
z właściwościami width i height. Następnie ustawiamy stan odpowiednio na szerokość i wysokość bieżącego okna, gdy okno jest zmieniane. Wyczyściliśmy również kod po odmontowaniu. Proces czyszczenia jest niezbędny w useLayoutEffect
do czyszczenia manipulacji DOM i poprawy wydajności.
- Rozmyjmy tekst za pomocą
useLayoutEffect
.
import { useRef, useState, useLayoutEffect } from "react"; export default function App() { const paragraphRef = useRef(""); useLayoutEffect(() => { const { current } = paragraphRef; const blurredEffect = () => { current.style.color = "transparent"; current.style.textShadow = "0 0 5px rgba(0,0,0,0.5)"; }; current.addEventListener("click", blurredEffect); return () => current.removeEventListener("click", blurredEffect); }, []); return ( <div className="App"> <p ref={paragraphRef}>This is the text to blur</p> </div> ); }
W powyższym kodzie użyliśmy razem useRef
i useLayoutEffect
. Najpierw utworzyliśmy ref, paragraphRef
, aby wskazywać na nasz akapit. Następnie utworzyliśmy detektor zdarzeń po kliknięciu, który monitoruje kliknięcie akapitu, a następnie zamazaliśmy go za pomocą zdefiniowanych przez nas właściwości stylu. Na koniec wyczyściliśmy detektor zdarzeń za pomocą removeEventListener
.
useDispatch
i useSelector
useDispatch
jest hakiem Redux do wywoływania (wyzwalania) akcji w aplikacji. Przyjmuje obiekt akcji jako argument i wywołuje akcję. useDispatch
jest odpowiednikiem hooka dla mapDispatchToProps
.
Z drugiej strony useSelector
jest hakiem Redux do oceny stanów Redux. Zajmuje funkcję, aby wybrać dokładny reduktor Redux ze sklepu, a następnie zwraca odpowiednie stany.
Po połączeniu naszego sklepu Redux z aplikacją React przez dostawcę Redux, możemy wywoływać akcje za pomocą useDispatch
i uzyskać dostęp do stanów za pomocą useSelector
. Każde działanie i stan Redux można ocenić za pomocą tych dwóch haków.
Zauważ, że te stany są dostarczane z React Redux (pakietem, który ułatwia ocenę sklepu Redux w aplikacji React). Nie są one dostępne w podstawowej bibliotece Redux.
Te haczyki są bardzo proste w użyciu. Najpierw musimy zadeklarować funkcję wysyłki, a następnie ją wywołać.
import {useDispatch, useSelector} from 'react-redux' import {useEffect} from 'react' const myaction from '...' const ReduxHooksExample = () =>{ const dispatch = useDispatch() useEffect(() => { dispatch(myaction()); //alternatively, we can do this dispatch({type: 'MY_ACTION_TYPE'}) }, []) const mystate = useSelector(state => state.myReducerstate) return( ... ) } export default ReduxHooksExample
W powyższym kodzie zaimportowaliśmy useDispatch
i useSelector
z react-redux
. Następnie w haczyku useEffect
uruchomiliśmy akcję. Możemy zdefiniować akcję w innym pliku, a następnie wywołać ją tutaj lub możemy zdefiniować ją bezpośrednio, jak pokazano w wywołaniu useEffect
.
Po wysłaniu akcji nasze stany będą dostępne. Następnie możemy pobrać stan za pomocą zaczepu useSelector
, jak pokazano. Stany mogą być używane w taki sam sposób, w jaki używamy stanów z haka useState
.
Spójrzmy na przykład, aby zademonstrować te dwa haki.
Aby zademonstrować tę koncepcję, musimy stworzyć sklep Redux, reduktor i akcje. Aby uprościć tutaj, będziemy używać biblioteki Redux Toolkit z naszą fałszywą bazą danych z JSONPlaceholder.
Aby rozpocząć, musimy zainstalować następujące pakiety. Uruchom następujące polecenia bash.
npm i redux @reduxjs/toolkit react-redux axios
Najpierw utwórzmy plik employeesSlice.js
do obsługi reduktora i akcji dla interfejsu API naszych pracowników.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import axios from "axios"; const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/employees"; export const fetchEmployees = createAsyncThunk("employees/fetchAll", async () => { const { data } = await axios.get(endPoint); return data; }); const employeesSlice = createSlice({ name: "employees", initialState: { employees: [], loading: false, error: "" }, reducers: {}, extraReducers: { [fetchEmployees.pending]: (state, action) => { state.status = "loading"; }, [fetchEmployees.fulfilled]: (state, action) => { state.status = "success"; state.employees = action.payload; }, [fetchEmployees.rejected]: (state, action) => { state.status = "error"; state.error = action.error.message; } } }); export default employeesSlice.reducer;
To jest standardowa konfiguracja zestawu narzędzi Redux. Użyliśmy metody createAsyncThunk
, aby uzyskać dostęp do oprogramowania pośredniczącego Thunk
w celu wykonywania działań asynchronicznych. To pozwoliło nam pobrać listę pracowników z API. Następnie utworzyliśmy employeesSlice
i zwróciliśmy, „ładowanie”, „błąd” oraz dane pracowników w zależności od rodzaju akcji.
Zestaw narzędzi Redux ułatwia również konfigurowanie sklepu. Oto sklep.
import { configureStore } from "@reduxjs/toolkit"; import { combineReducers } from "redux"; import employeesReducer from "./employeesSlice"; const reducer = combineReducers({ employees: employeesReducer }); export default configureStore({ reducer });;
W tym przypadku użyliśmy combineReducers
do połączenia reduktorów i funkcji configureStore
dostarczanej przez zestaw narzędzi Redux do skonfigurowania sklepu.
Przejdźmy do wykorzystania tego w naszej aplikacji.
Najpierw musimy połączyć Redux z naszą aplikacją React. Najlepiej byłoby to zrobić w katalogu głównym naszej aplikacji. Lubię to robić w index.js
.
import React, { StrictMode } from "react"; import ReactDOM from "react-dom"; import store from "./redux/store"; import { Provider } from "react-redux"; import App from "./App"; const rootElement = document.getElementById("root"); ReactDOM.render( <Provider store={store}> <StrictMode> <App /> </StrictMode> </Provider>, rootElement );
Tutaj zaimportowałem sklep, który stworzyłem powyżej, a także Provider
z react-redux
.
Następnie całą aplikację owinąłem funkcją Provider
, przekazując do niej sklep. Dzięki temu sklep jest dostępny w całej naszej aplikacji.
Następnie możemy przystąpić do korzystania z useDispatch
i useSelector
do pobrania danych.
Zróbmy to w naszym pliku App.js
import { useDispatch, useSelector } from "react-redux"; import { fetchEmployees } from "./redux/employeesSlice"; import { useEffect } from "react"; export default function App() { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchEmployees()); }, [dispatch]); const employeesState = useSelector((state) => state.employees); const { employees, loading, error } = employeesState; return ( <div className="App"> {loading ? ( "Loading..." ) : error ? ( <div>{error}</div> ) : ( <> <h1>List of Employees</h1> {employees.map((employee) => ( <div key={employee.id}> <h3>{`${employee.firstName} ${employee.lastName}`}</h3> </div> ))} </> )} </div> ); }
W powyższym kodzie użyliśmy haka useDispatch
do wywołania akcji fetchEmployees
utworzonej w pliku employeesSlice.js
. Dzięki temu stan pracowników jest dostępny w naszej aplikacji. Następnie użyliśmy zaczepu useSelector
do pobrania stanów. Następnie wyświetlaliśmy wyniki, mapując employees
.
Hak useHistory
Nawigacja jest bardzo ważna w aplikacji React. Chociaż można to osiągnąć na kilka sposobów, React Router zapewnia prosty, wydajny i popularny sposób na osiągnięcie dynamicznego routingu w aplikacji React. Co więcej, React Router zapewnia kilka punktów zaczepienia do oceny stanu routera i wykonywania nawigacji w przeglądarce, ale aby z nich korzystać, musisz najpierw poprawnie skonfigurować aplikację.
Aby użyć dowolnego haka React Router, powinniśmy najpierw zawinąć naszą aplikację w BrowserRouter
. Następnie możemy zagnieździć trasy za pomocą funkcji Switch
i Route
.
Ale najpierw musimy zainstalować pakiet, uruchamiając następujące polecenia.
npm install react-router-dom
Następnie musimy skonfigurować naszą aplikację w następujący sposób. Lubię to robić w moim pliku App.js
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import Employees from "./components/Employees"; export default function App() { return ( <div className="App"> <Router> <Switch> <Route path='/'> <Employees /> </Route> ... </Switch> </Router> </div> ); }
Możemy mieć tyle tras, ile to możliwe, w zależności od liczby komponentów, które chcemy renderować. Tutaj wyrenderowaliśmy tylko komponent Employees
. Atrybut path
informuje React Router DOM o ścieżce komponentu i może być oceniany za pomocą ciągu zapytania lub różnych innych metod.
Tutaj kolejność ma znaczenie. Trasa główna powinna być umieszczona poniżej trasy podrzędnej i tak dalej. Aby zastąpić tę kolejność, musisz podać exact
słowo kluczowe w trasie głównej.
<Route path='/' exact > <Employees /> </Route>
Teraz, gdy mamy skonfigurowany router, możemy użyć useHistory
i innych podpięć React Router w naszej aplikacji.
Aby użyć haka useHistory
, musimy go najpierw zadeklarować w następujący sposób.
import {useHistory} from 'history' import {useHistory} from 'react-router-dom' const Employees = () =>{ const history = useHistory() ... }
Jeśli logujemy historię do konsoli, zobaczymy kilka powiązanych z nią właściwości. Należą do nich block
, createHref
, go
, goBack
, goForward
, length
, listen
, location
, push
, replace
. Chociaż wszystkie te właściwości są przydatne, najprawdopodobniej będziesz używać history.push
i history.replace
częściej niż innych właściwości.
Użyjmy tej właściwości, aby przejść z jednej strony na drugą.
Zakładając, że chcemy pobrać dane o konkretnym pracowniku, gdy klikniemy w jego nazwiska. Za pomocą haka useHistory
możemy przejść do nowej strony, na której będą wyświetlane informacje o pracowniku.
function moveToPage = (id) =>{ history.push(`/employees/${id}`) }
Możemy to zaimplementować w naszym pliku Employee.js
, dodając następujące.
import { useEffect } from "react"; import { Link, useHistory, useLocation } from "react-router-dom"; export default function Employees() { const history = useHistory(); function pushToPage = (id) => { history.push(`/employees/${id}`) } ... return ( <div> ... <h1>List of Employees</h1> {employees.map((employee) => ( <div key={employee.id}> <span>{`${employee.firstName} ${employee.lastName} `}</span> <button onClick={pushToPage(employee.id)}> » </button> </div> ))} </div> ); }
W funkcji pushToPage
wykorzystaliśmy history
z useHistory
, aby przejść do strony pracownika i przekazać obok identyfikator pracownika.
Hak useLocation
Ten hak jest również dostarczany z React Router DOM. Jest to bardzo popularny hak używany do pracy z parametrem ciągu zapytania. Ten haczyk jest podobny do window.location
w przeglądarce.
import {useLocation} from 'react' const LocationExample = () =>{ const location = useLocation() return ( ... ) } export default LocationExample
useLocation
zwraca pathname
, parametr search
, hash
i state
. Najczęściej używane parametry to pathname
i search
, ale równie dobrze można użyć hash
i state
dużo w aplikacji.
Właściwość pathname
lokalizacji zwróci ścieżkę, którą ustawiliśmy w naszej konfiguracji Route
. Podczas search
zwróci parametr wyszukiwania zapytania, jeśli taki istnieje. Na przykład, jeśli przekażemy 'http://mywebsite.com/employee/?id=1'
do naszego zapytania, nazwą pathname
będzie /employee
, a search
będzie miało ?id=1
.
Możemy następnie pobrać różne parametry wyszukiwania za pomocą pakietów takich jak zapytanie-ciąg lub kodując je.
Hak useParams
Jeśli ustawimy naszą trasę z parametrem URL w atrybucie path, możemy ocenić te parametry jako pary klucz/wartość za pomocą haka useParams
.
Załóżmy na przykład, że mamy następującą trasę.
<Route path='/employees/:id' > <Employees /> </Route>
Trasa będzie oczekiwała dynamicznego identyfikatora zamiast :id
.
Za pomocą haka useParams
możemy ocenić identyfikator przekazany przez użytkownika, jeśli taki istnieje.
Na przykład, zakładając, że użytkownik przekazuje następujące funkcje w funkcji history.push
,
function goToPage = () => { history.push(`/employee/3`) }
Możemy użyć haka useParams
, aby uzyskać dostęp do tego parametru adresu URL w następujący sposób.
import {useParams} from 'react-router-dom' const ParamsExample = () =>{ const params = useParams() console.log(params) return( <div> ... </div> ) } export default ParamsExample
Jeśli params
parametry do konsoli, otrzymamy następujący obiekt {id: "3"}
.
Hak useRouteMatch
Ten hak zapewnia dostęp do obiektu dopasowania. Zwraca najbliższe dopasowanie do komponentu, jeśli nie podano do niego argumentu.
Obiekt match zwraca kilka parametrów, w tym path
(taką samą jak ścieżka określona w Route), URL
, obiekt params
i isExact
.
Na przykład możemy użyć useRouteMatch
, aby zwrócić komponenty na podstawie trasy.
import { useRouteMatch } from "react-router-dom"; import Employees from "..."; import Admin from "..." const CustomRoute = () => { const match = useRouteMatch("/employees/:id"); return match ? ( <Employee /> ) : ( <Admin /> ); }; export default CustomRoute;
W powyższym kodzie ustawiamy ścieżkę trasy za pomocą useRouteMatch
, a następnie renderujemy komponent <Employee />
lub <Admin />
w zależności od trasy wybranej przez użytkownika.
Aby to zadziałało, nadal musimy dodać trasę do naszego pliku App.js
... <Route> <CustomRoute /> </Route> ...
Budowanie niestandardowego haka
Zgodnie z dokumentacją React, zbudowanie niestandardowego hooka pozwala nam wyodrębnić logikę do funkcji wielokrotnego użytku. Musisz jednak upewnić się, że wszystkie reguły, które mają zastosowanie do hooków Reacta, mają zastosowanie do twojego niestandardowego hooka. Sprawdź zasady haka React na początku tego samouczka i upewnij się, że twój niestandardowy hak jest zgodny z każdym z nich.
Niestandardowe hooki pozwalają nam napisać funkcje raz i używać ich ponownie, gdy są potrzebne, a tym samym przestrzegać zasady DRY.
Na przykład moglibyśmy utworzyć niestandardowy zaczep, aby uzyskać pozycję przewijania na naszej stronie w następujący sposób.
import { useLayoutEffect, useState } from "react"; export const useScrollPos = () => { const [scrollPos, setScrollPos] = useState({ x: 0, y: 0 }); useLayoutEffect(() => { const getScrollPos = () => setScrollPos({ x: window.pageXOffset, y: window.pageYOffset }); window.addEventListener("scroll", getScrollPos); return () => window.removeEventListener("scroll", getScrollPos); }, []); return scrollPos; };
Tutaj zdefiniowaliśmy niestandardowy zaczep, aby określić pozycję przewijania na stronie. Aby to osiągnąć, najpierw utworzyliśmy stan scrollPos
, aby przechowywać pozycję przewijania. Ponieważ będzie to modyfikowanie DOM, musimy użyć useLayoutEffect
zamiast useEffect
. Dodaliśmy detektor zdarzeń przewijania do przechwytywania pozycji przewijania xiy, a następnie wyczyściliśmy detektor zdarzeń. Wreszcie wróciliśmy do pozycji przewijania.
Możemy użyć tego niestandardowego haka w dowolnym miejscu naszej aplikacji, wywołując go i używając go tak, jak używalibyśmy każdego innego stanu.
import {useScrollPos} from './Scroll' const App = () =>{ const scrollPos = useScrollPos() console.log(scrollPos.x, scrollPos.y) return ( ... ) } export default App
Tutaj zaimportowaliśmy niestandardowy hook useScrollPos
, który stworzyliśmy powyżej. Następnie zainicjowaliśmy go, a następnie zarejestrowaliśmy wartość na naszej konsoli. Jeśli przewijamy stronę, hak pokaże nam pozycję przewijania na każdym kroku przewijania.
Możemy tworzyć niestandardowe haki, aby zrobić prawie wszystko, co możemy sobie wyobrazić w naszej aplikacji. Jak widać, do wykonania pewnych funkcji wystarczy użyć wbudowanego haka React. Możemy również użyć bibliotek innych firm do tworzenia niestandardowych podpięć, ale jeśli to zrobimy, będziemy musieli zainstalować tę bibliotekę, aby móc korzystać z podpięcia.
Wniosek
W tym samouczku przyjrzeliśmy się kilku przydatnym hookom Reacta, którego będziesz używać w większości swoich aplikacji. Zbadaliśmy, co prezentują i jak je wykorzystać w swojej aplikacji. Przyjrzeliśmy się również kilku przykładom kodu, które pomogą Ci zrozumieć te zaczepy i zastosować je w Twojej aplikacji.
Zachęcam do wypróbowania tych hooków we własnej aplikacji, aby lepiej je zrozumieć.
Zasoby z React Docs
- Najczęściej zadawane pytania dotyczące haków
- Zestaw narzędzi Redux
- Korzystanie z haczyka stanu
- Korzystanie z haczyka efektów
- Informacje o interfejsie API haków
- React Redux Hooki
- Reaguj haki routera