Logiikan uudelleenkäyttö omilla Hookeilla

React sisältää useita sisäänrakennettuja Hookkeja kuten useState, useContext, ja useEffect. Joskus saatat haluta, että olisi Hookki johonkin tiettyyn tarkoitukseen: esimerkiksi, datan hakemiseen, käyttäjän verkkoyhteyden seuraamiseen, tai yhteyden muodostamiseen chat-huoneeseen. Et välttämättä löydä näitä Hookkeja Reactista, mutta voit luoda omia Hookkeja sovelluksesi tarpeisiin.

Tulet oppimaan

  • Mitä omat Hookit ovat ja miten voit kirjoittaa niitä
  • Miten voit jakaa logiikkaa komponenttien välillä
  • Miten nimetä ja järjestää omat Hookit
  • Milloin ja miksi omat Hookit kannattaa tehdä

Omat Hookit: Logiikan jakaminen komponenttien välillä

Kuvittele, että olet kehittämässä sovellusta, joka tukeutuu paljolit verkkoon (kuten useimmat sovellukset). Haluat varoittaa käyttäjää, jos heidän verkkoyhteytensä on vahingossa katkennut, kun he käyttivät sovellustasi. Miten lähestyisit tätä? Näyttää siltä, että tarvitset kaksi asiaa komponentissasi:

  1. Palan tilaa, joka seuraa onko verkkoyhteys saatavilla.
  2. Efektin, joka tilaa globaalin online ja offline tapahtumat, ja päivittää tilan.

Tämä pitää komponenttisi synkronoituna verkon tilan kanssa. Voit aloittaa tällaisella:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

Kokeile yhdistää verkko päälle ja pois, ja huomaa miten StatusBar päivittyy toimintasi mukaan.

Kuvittele nyt, että haluat myös käyttää samaa logiikkaa toisessa komponentissa. Haluat toteuttaa Tallenna -painikkeen, joka menee pois käytöstä ja näyttää “Yhdistetään…” sen sijaan, että se näyttäisi “Tallenna” kun verkko on pois päältä.

Aloittaaksesi, voit kopioida ja liittää isOnline tilan ja Efektin SaveButtoniin:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

Varmista, että jos käännät verkon pois päältä, painike muuttaa ulkonäköään.

Nämä kaksi komponenttia toimivat, mutta niiden logiikan kopiointi on valitettavaa. Vaikuttaa siltä, että vaikka niillä on erilainen visuaalinen ulkonäkö, haluat jakaa niiden logiikkaa.

Oman Hookin tekeminen komponentista

Kuvttele, että samalla tavalla kuin useState ja useEffect, olisi olemassa sisäänrakennettu useOnlineStatus Hookki. Sitten molemmat näistä komponenteista voitaisiin yksinkertaistaa ja voit poistaa niiden toistetun logiikan:

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progress saved');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}

Vaikka tällaista sisäänrakennettua Hookkia ei ole, voit kirjoittaa sen itse. Määrittele funktio nimeltä useOnlineStatus ja siirrä kaikki toistettu koodi komponenteista siihen:

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

Funktion lopussa, palauta isOnline. Tämä antaa komponenttien lukea arvoa:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

Vahvista, että verkon kytkeminen päälle ja pois päältä päivittää molemmat komponentit.

Nyt komponenttisi ei sisällä niin paljon toistettua logiikkaa. Tärkeämpää on, että niiden sisällä oleva koodi kuvailee mitä ne haluavat tehdä (käyttää verkon tilaa!) sen sijaan, että miten se tehdään (tilaamalla selaimen tapahtumia).

Kun siirrät logiikan omiin Hookkeihin, voit piilottaa miten käsittelet jotain ulkoista järjestelmää tai selaimen API:a. Komponenttisi koodi ilmaisee aikomuksesi, ei toteutusta.

Hookkien nimet alkavat aina use -etuliitteellä

React sovellukset rakennetaan komponenteista. Komponentit ovat rakennettu Hookeista, sisäänrakennetuista tai omista. Todennäköisesti käytät usein muiden tekemiä omia Hookkeja, mutta joskus saatat kirjoittaa oman!

Sinun täytyy noudattaa näitä nimeämiskäytäntöjä:

  1. React komponenttien nimien on alettava isolla alkukirjaimella, kuten StarBar ja SaveButton. React komponenttien täytyy myös palauttaa jotain, mitä React osaa näyttää, kuten JSX-palasen.
  2. Hookkien nimien on alettava use etuliitteellä, jota seuraa iso alkukirjain, kuten useState (sisäänrakennettu) tai useOnlineStatus (oma, kuten aiemmin sivulla). Hookit voivat palauttaa mitä tahansa arvoja.

Tämä yleinen tapa takaa sen, että voit aina katsoa komponenttia ja tiedät missä kaikki sen tila, Efekti, ja muut React toiminnot saatat “piiloutua”. Esimerkiksi, jos näet getColor() funktiokutsun komponentissasi, voit olla varma, että se ei voi sisältää React tilaa sisällä koska sen nimi ei ala use -etuliitteellä. Kuitenkin, funktiokutsu kuten useOnlineStatus() todennäköisesti sisältää kutsuja muihin Hookkeihin sen sisällä!

Huomaa

Jos linterisi on määritelty Reactille, se takaa tämän nimeämiskäytännön. Selaa yllä olevaan esimerkkiin ja nimeä useOnlineStatus uudelleen getOnlineStatus:ksi. Huomaa, että linteri ei enää salli sinun kutsua useState tai useEffect -funktioita sen sisällä. Vain Hookit ja komponentit voivat kutsua muita Hookkeja!

Syväsukellus

Pitäisikö kaikkien renderöinnin aikana kutsuttujen funktioiden käyttää use -etuliitettä?

Funktiot, jotka eivät kutsu Hookkeja eivät tarvitse olla Hookkeja.

Jos funktiosi ei kutsu yhtään Hookkia, vältä use etuliitteen käyttöä. Sen sijaan, kirjoita se kuten tavallinen funktio ilman use etuliitettä. Esimerkiksi, alla oleva useSorted ei kutsu Hookkeja, joten sen sijaan kutsu sitä getSorted nimellä:

// 🔴 Vältä: Hookki, joka ei käytä Hookkeja
function useSorted(items) {
return items.slice().sort();
}

// ✅ Hyvä: Tavallinen funktio, joka ei käytä Hookkeja
function getSorted(items) {
return items.slice().sort();
}

Tämä takaa sen, että koodisi voi kutsua tätä funktiota missä tahansa, mukaan lukien ehtolauseissa:

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ On ok kutsua getSorted() ehdollisesti, koska se ei ole Hookki
displayedItems = getSorted(items);
}
// ...
}

Anna use etuliite funktiolle (ja siten tee siitä Hookki) jos se käyttää edes yhtä Hookkia sen sisällä:

// ✅ Hyvä: Hookki, joka käyttää muita Hookkeja
function useAuth() {
return useContext(Auth);
}

Teknisesti ottaen tätä ei pakoteta Reactissa. Periaatteessa, voit tehdä Hookin, joka ei kutsu muita Hookkeja. Tämä on usein hämmentävää ja rajoittavaa, joten on parasta välttää tätä mallia. Kuitenkin, voi olla harvinaisia tapauksia, joissa se on hyödyllistä. Esimerkiksi, ehkä funktiosi ei käytä yhtään Hookkia juuri nyt, mutta suunnittelet lisääväsi siihen Hookkien kutsuja tulevaisuudessa. Silloin on järkevää nimetä se use etuliitteellä:

// ✅ Hyvä: Hoookki, joka saattaa kutsua toisia Hookkeja myöhemmin
function useAuth() {
// TODO: Korvaa tämä rivi kun autentikointi on toteutettu:
// return useContext(Auth);
return TEST_USER;
}

Silloin komponentit eivät voi kutsua sitä ehdollisesti. Tästä tulee tärkeää kun haluat lisätä Hookkien kutsuja sen sisään. Jos et suunnittele lisääväsi Hookkien kutsuja sen sisään (taikka myöhemmin), älä tee siitä Hookkia.

Omien Hookkien avulla voit jakaa tilallista logiikkaa, et tilaa suoraan

Aiemmassa esimerkissä, kun käänsit verkon päälle ja pois päältä, molemmat komponentit päivittyivät yhdessä. Kuitenkin, on väärin ajatella, että yksi isOnline tilamuuttuja on jaettu niiden välillä. Katso tätä koodia:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Se toimii samalla tavalla kuin ennen toistetun logiikan poistamista:

function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

Nämä ovat kaksi täysin toisistaan erillä olevia tilamuuttujia ja Effekteja! Ne sattuivat olemaan saman arvoisia samaan aikaan koska synkronoit ne samalla ulkoisella arvolla (onko verkko päällä).

Tämän paremmin havainnollistaaksesi, tarvitsemme erilaisen esimerkin. Kuvittele tämä Form komponentti:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

Jokaisessa lomakkeen kentässä on toistettua logiikkaa:

  1. On pala tilaa: (firstName and lastName).
  2. On tapahtumankäsittelijöitä: (handleFirstNameChange and handleLastNameChange).
  3. On pala JSX koodia, joka määrittelee value:n jaonChange attribuutin syöttökentälle.

Voit siirtää toistuvan logiikan tästä useFormInput omaksi Hookiksi:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

Huomaa miten se vain määrittelee yhden tilamuuttujan nimeltä value.

Kuitenkin, Form komponentti kutsuu useFormInput:a kahdesti:

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

Tämän takia se toimii kuten kaksi erillistä tilamuuttujaa!

*Omien Hookkien avulla voit jakaa tilallista logiikkaa’ muttet tilaa itsessään. Jokainen kutsu Hookkiin on täysin eristetty toisista kutsuista samaan Hookkiin. Tämän takia kaksi yllä olevaa hiekkalaatikkoa ovat täysin samanlaisia. Jos haluat, selaa ylös ja vertaa niitä. Käyttäytyminen ennen ja jälkeen oman Hookin tekemiseen on identtinen.

Kun haluat jakaa tilaa kahden komponentin välillä, nosta se ylös ja välitä se alaspäin.

Reaktiivisten arvojen välittäminen Hookkien välillä

Koodi oman Hookkisi sisällä suoritetaan joka kerta komponentin renderöinnin yhteydessä. Tämän takia, kuten komponenttien, omien Hookkien täytyy olla puhtaita. Ajattele oman Hookkisi koodia osana komponenttisi sisältöä!

Koska omat Hookit renderöidään yhdessä komponenttisi kanssa, ne saavat aina uusimmat propit ja tilan. Katso mitä tämä tarkoittaa, harkitse tätä chat-huone esimerkkiä. Muuta palvelimen URL tai chat-huone:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Kun muutat serverUrl tai roomId, Efekti “reagoi” muutoksiisi ja synkronoituu uudelleen. Voit nähdä tämän konsoliviesteistä, että chat yhdistää uudelleen joka kerta kun muutat Efektin riippuvuuksia.

Nyt siirrä Efektin koodi omaan Hookkiisi:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Tämän avulla ChatRoom komponenttisi kutsuu omaa Hookkiasi huolimatta siitä miten se toimii:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});

return (
<>
<label>
Server URL:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}

Tämä näyttää paljon yksinkertaisemmalta! (Mutta tekee saman asian.)

Huomaa miten logiikka silti reagoi propsin ja tilan muutoksiin. Kokeile muokata palvelimen URL tai valittua huonetta:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Huomaa miten, otat yhden Hookin palautusarvon:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

ja välität sen toisen Hookin sisään:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

Joka kerta kun ChatRoom komponenttisi renderöityy, se välittää viimeisimmän roomId ja serverUrl:n Hookillesi. Tämän takia Efektisi yhdistää chattiin joka kerta kun niiden arvot muuttuvat edellisestä renderöinnistä. (Jos olet koskaan työskennellyt ääni- tai videokäsittelyohjelmistojen kanssa, Hookkien ketjuttaminen saattaa muistuttaa sinua visuaalisten tai ääniefektien ketjuttamisesta. Se on kuin useState -tulosteen “syöttäminen” useChatRoom -syötteeseen.)

Tapahtumankäsittelijöiden välittäminen omiin Hookkeihin

Kehitteillä

Tämä osio kuvailee kokeellista API:a, joka ei ole vielä julkaistu vakaassa React versiossa.

Kun alat käyttämään useChatRoom Hookkia useammissa komponenteissa, saatat haluta antaa komponenttien muokata sen toimintaa. Esimerkiksi, tällä hetkellä, logiikka sille mitä tehdä kun viesti saapuu on kovakoodattu Hookkiin:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Sanotaan, että haluat siirtää tämän logiikan takaisin komponenttiisi:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
// ...

Saadaksesi tämä toimimaan, muuta oma Hookkisi vastaanottamaan onReceiveMessage yhtenä nimetyistä vaihtoehdoista:

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ Kaikki muuttujat määritelty
}

Tämä silti toimii, mutta on yksi parannus, jonka voit tehdä kun oma Hookkisi hyväksyy tapahtumankäsittelijöitä.

onReceiveMessage riippuvuuden lisääminen ei ole ihanteellista, koska se aiheuttaa chattiin yhdistämisen joka kerta kun komponentti renderöityy. Kääri tämä tapahtumankäsittelijä Efektitapahtumaan poistaaksesi sen riippuvuuksista:

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Kaikki riippuvuudet määritelty
}

Nyt chatti ei enää yhdistä uudelleen joka kerta, kun ChatRoom komponentti renderöidään uudelleen. Tässä on toimiva esimerkki tapahtumankäsittelijän välittämisestä omiin Hookkeihin, jota voit kokeilla:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Huomaa miten sinun ei tarvitse enää tietää miten useChatRoom toimii käyttääksesi sitä. Voisit lisätä sen mihin tahansa muuhun komponenttiin, välittää mitä tahansa muita vaihtoehtoja, ja se toimisi samalla tavalla. Tämä on omien Hookkien voima.

Milloin käyttää omia Hookkeja

Sinun ei tarvitse luoda omaa Hookkia jokaiselle toistetulle koodinpalaselle. Jotkut toistot ovat hyväksyttäviä. Esimerkiksi, oman useFormInput Hookin luominen yhden useState kutsun ympärille kuten aiemmin on todennäköisesti tarpeetonta.

Kuitenkin, joka kerta kun kirjoitat Efektiä, mieti olisiko selkeämpää kääriä se omaan Hookkiin. Sinun ei tulisi tarvita Efektejä usein, joten jos olet kirjoittamassa yhtä, se tarkoittaa että sinun tulee “astua ulos Reactista” synkronoidaksesi jonkin ulkoisen järjestelmän kanssa tai tehdäksesi jotain, jolle Reactilla ei ole sisäänrakennettua API:a. Käärimällä sen omaan Hookkiin voit tarkasti kommunikoida aikeesi ja miten data virtaa sen läpi.

Esimerkiksi, harkitse ShippingForm komponenttia, joka näyttää kaksi pudotusvalikkoa: toinen näyttää kaupunkien listan, ja toinen näyttää valitun kaupungin alueiden listan. Voit aloittaa koodilla, joka näyttää tältä:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// Tämä Efekti hakee kaupungit maalle
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// Tämä Efekti hakee alueet valitulle kaupungille
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);

// ...

Vaikka tämä koodi on toistuvaa, on oiken pitää nämä Efektit erillään toisistaan. Ne synkronoivat kahta eri asiaa, joten sinun ei tulisi yhdistää niitä yhdeksi Efektiksi. Sen sijaan, voit yksinkertaistaa ShippingForm komponenttia yllä käärimällä yhteisen logiikan omaksi useData Hookiksi:

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}

Nyt voit korvata molemmat Efektit ShippingForm komponentissa useData kutsuilla:

function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...

Omien Hookkien tekeminen tekee datavirtauksesta eksplisiittisempää. Syötät url arvon sisään ja saat data:n ulos. “Piilottamalla” Efektin useData:n sisään, vältät myös sen, että joku joka työskentelee ShippingForm komponentin kanssa lisää turhia riippuvuuksia siihen. Ajan myötä, suurin osa sovelluksesi Efekteistä on omien Hookkien sisällä.

Syväsukellus

Pidä Hookkisi konkreettisissa korkean tason käyttötapauksissa

Aloita valitsemalla oman Hookkisi nimi. Jos sinulla on vaikeuksia valita selkeä nimi, se saattaa tarkoittaa, että Efektisi on liian kytketty komponenttisi logiikkaan, eikä ole vielä valmis eristettäväksi.

Ihanteellisesti, oman Hookkisi nimi tulisi olla tarpeeksi selkeä, että jopa henkilö joka ei kirjoita koodia usein voisi arvata mitä oma Hookkisi tekee, mitä se ottaa vastaan, ja mitä se palauttaa:

  • useData(url)
  • useImpressionLog(eventName, extraData)
  • useChatRoom(options)

Kun synkronoit ulkoisen järjestelmän kanssa, oman Hookkisi nimi saattaa olla teknisempi ja käyttää kyseisen järjestelmän jargonia. On hyvä asia, kunhan se olisi selvää henkilölle joka on tuttu kyseisen järjestelmän kanssa:

  • useMediaQuery(query)
  • useSocket(url)
  • useIntersectionObserver(ref, options)

Pidä omat Hookkisi keskittyneinä konkreettisiin korkean tason käyttötapauksiin. Vältä luomasta ja käyttämästä omia elinkaaren Hookkeja, jotka toimivat vaihtoehtoina ja kätevinä kääreinä useEffect API:lle:

  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

Esimerkiksi, tämä useMount Hookki pyrkii takamaan, että jotain koodia suoritetaan vain “mountissa”:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// 🔴 Vältä: käyttämästä omia elinkaaren Hookkeja
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔴 Vältä: luomasta omia elinkaaren Hookkeja
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
}

Omat “elinkaaren” Hookit kuten useMount eivät sovi hyvin Reactin paradigman kanssa. Esimerkiksi, tässä koodissa on virhe (se ei “reagoi” roomId tai serverUrl muutoksiin), mutta linteri ei varoita sinua siitä, koska linteri tarkistaa vain suoria useEffect kutsuja. Se ei tiedä omasta Hookistasi.

Jos olet kirjoittamassa Efektiä, aloita käyttämällä React APIa suoraan:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Hyvä: kaksi raakaa Efektiä jaettu eri tarkoituksiin

useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

Sitten, voit (mutta sinun ei tarvitse) eristää omia Hookkeja eri korkean tason käyttötapauksille:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Hyvä: omat Hookit nimetty tarkoitusten perusteella
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

Hyvä Hookki tekee koodin kutsumisesta deklaratiivisempaa rajoittamalla mitä se tekee. Esimerkiksi, useChatRoom(options) voi vain yhdistää chattiin, kun taas useImpressionLog(eventName, extraData) voi vain lähettää näyttökerran analytiikkaan. Jos oma Hookkisi API ei rajoita käyttötapauksia ja on hyvin abstrakti, pitkällä aikavälillä se todennäköisesti aiheuttaa enemmän ongelmia kuin ratkaisee.

Omat Hookit auttavat siirtymään parempiin toimintatapoihin

Efektit ovat “pelastusluukku”: käytät niitä kun sinun täytyy “astua ulos Reactista” ja kun parempaa sisäänrakennettua ratkaisua käyttötapaukseesi ei ole. Ajan myötä, React tiimin tavoite on vähentää Efektien määrää sovelluksessasi minimiin tarjoamalla tarkempia ratkaisuja tarkempiin ongelmiin. Efektiesi kääriminen omiin Hookkeihin tekee koodin päivittämisestä helpompaa kun nämä ratkaisut tulevat saataville.

Palataan tähän esimerkkiin:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

Yllä olevassa esimerkissä, useOnlineStatus on toteutettu useState ja useEffect. Hookeilla. Kuitenkin, tämä ei ole paras ratkaisu. On useita reunatapauksia, joita se ei huomioi. Esimerkiksi, se olettaa komponentin mountatessa, isOnline olisi jo true, vaikka tämä voi olla väärin jos verkkoyhteys on jo katkennut. Voit käyttää selaimen navigator.onLine API:a tarkistaaksesi tämän, mutta sitä ei voi käyttää suoraan palvelimella HTML:n generointiin. Lyhyesti, tätä koodia voisi parantaa.

Onneksi, React 18 sisältää dedikoidun APIn nimeltään useSyncExternalStore, joka huolehtii kaikista näistä ongelmista puolestasi. Tässä on miten useOnlineStatus Hookkisi kirjoitetaan uudelleen hyödyntämään tätä uutta API:a:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // Miten haet arvon päätelaitteella
    () => true // Miten haet arvon palvelimella
  );
}

Huomaa miten sinun ei tarvinnut muuttaa mitään komponenteissa tehdäksesi tämän siirtymän:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Tämä on yksi syy miksi Efektien kääriminen omiin Hookkeihin on usein hyödyllistä:

  1. Teet datavirtauksesta Efektiin ja Efektistä eksplisiittistä.
  2. Annat komponenttien keskittyä tarkoitukseen tarkan Efektin toteutuksen sijaan.
  3. Kun React lisää uusia ominaisuuksia, voit poistaa nämä Efektit muuttamatta komponenntejasi.

Samoin kuin design -järjestelmissä, saatat kokea yleisten ilmaisujen eristämisen omista komponenteista omiin Hookkeihin hyödylliseksi. Tämä pitää komponenttiesi koodin keskittyneenä tarkoitukseen, ja antaa sinun välttää raakojen Efektien kirjoittamista hyvin usein. Monia erinomaisia omia Hookkeja ylläpitää Reactin yhteisö.

Syväsukellus

Tuleeko React tarjoamaan sisäänrakennetun ratkaisun tiedonhakuun?

Työstämme yksityiskohtia, mutta odotamme että tulevaisuudessa, kirjoitat datan hakemisen näin:

import { use } from 'react'; // Ei vielä saatavilla!

function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...

Jos käytät omia Hookkeja kuten useData yllä sovelluksessasi, se vaatii vähemmän muutoksia siirtyä lopulta suositeltuun lähestymistapaan kuin jos kirjoitat raakoja Efektejä jokaiseen komponenttiin manuaalisesti. Kuitenkin, vanha lähestymistapa toimii edelleen hyvin, joten jos tunnet olosi onnelliseksi kirjoittaessasi raakoja Efektejä, voit jatkaa niiden käyttämistä.

On useampi tapa tehdä se

Sanotaan, että haluat toteuttaa häivitysanimaation alusta saakka käyttäen selaimen requestAnimationFrame APIa. Saatat aloittaa Efektillä joka asettaa animaatiosilmukan. Jokaisen animaatiokehyksen aikana, voisit muuttaa DOM solmun läpinäkyvyyttä, jonka pidät ref:ssä kunnes se saavuttaa 1. Koodisi saattaisi alkaa näyttää tältä:

import { useState, useEffect, useRef } from 'react';

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // On silti enemmän kehyksiä tehtävänä
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Tehdäksesi komponentista luettavemman, saatat eristää logiikan useFadeIn omaksi Hookiksi:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Voit pitää useFadeIn koodin sellaisenaan, mutta voit myös refaktoroida sitä enemmän. Esimerkiksi, voit eristää logiikan animaatiosilmukan asettamisen useFadeIn ulkopuolelle omaksi useAnimationLoop Hookiksi:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

Kuitenkaan sinun ei ole pakko tehdä sitä. Kuten tavallisten funktioiden kanssa, lopulta päätät missä piirrät rajat eri osien välille koodissasi. Voit myös ottaa hyvin erilaisen lähestymistavan. Sen sijaan, että pitäisit logiikan Efektissä, voit siirtää suurimman osan imperatiivisesta logiikasta JavaScript luokkaan:

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

Efektien avulla yhdistät Reactin ulkoisiin järjestelmiin. Mitä enemmän koordinaatiota Efektien välillä tarvitaan (esimerkiksi, ketjuttaaksesi useita animaatioita), sitä enemmän on järkeä eristää logiikka Efekteistä ja Hookkeista täysin kuten yllä olevassa esimerkissä. Sitten, eristämäsi koodi tulee “ulkoiseksi järjestelmäksi”. Tämä pitää Efektisi yksinkertaisina koska niiden täytyy vain lähettää viestejä järjestelmään jonka olet siirtänyt Reactin ulkopuolelle.

Esimerkki yllä olettaa, että häivityslogiikka täytyy kirjoittaa JavaScriptillä. Kuitenkin, tämä tietty häivitysanimaatio on sekä yksinkertaisempi että paljon tehokkaampi toteuttaa tavallisella CSS animaatiolla:

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Joskus et edes tarvitse Hookkia!

Kertaus

  • Omien Hookkien avulla voit jakaa logiikkaa komponenttien välillä.
  • Omat Hookit on nimettävä use-alkuisiksi ja niiden täytyy alkaa isolla kirjaimella.
  • Omat Hookit jakavat vain tilallisen logiikan, ei itse tilaa.
  • Voit välittää reaktiivisia arvoja Hookista toiseen ja ne pysyvät ajan tasalla.
  • Kaikki Hookit suoritetaan joka kerta kun komponenttisi renderöityy.
  • Hookin koodin tulisi olla puhdasta, kuten komponenttisi koodi.
  • Kääri tapahtumankäsittelijät jotka Hookkisi vastaanottaa Efektitapahtumiin.
  • Älä luo omia Hookkeja kuten useMount. Pidä niiden tarkoitus tarkkana.
  • Sinä päätät miten ja missä valitset koodisi rajat.

Haaste 1 / 5:
Tee useCounter Hookki

Tämä komponentti käyttää tilamuuttujaa ja Efektiä näyttääkseen numeron joka kasvaa joka sekunti. Eristä tämä logiikka omaksi Hookiksi nimeltä useCounter. Tavoitteesi on saada Counter komponentin toteutus näyttämään tältä:

export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}

Sinun täytyy kirjoittaa oma Hookkisi useCounter.js tiedostoon ja tuoda se Counter.js tiedostoon.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Seconds passed: {count}</h1>;
}