Et ehkä tarvitse Effectia
Efektit ovat pelastusluukku React-paradigmasta. Niiden avulla voit “astua ulos” Reactista ja synkronoida komponenttejasi jonkin ulkoisen järjestelmän, kuten ei-React-widgetin, verkon tai selaimen DOM:in kanssa. Jos ulkoista järjestelmää ei ole mukana (esimerkiksi jos haluat päivittää komponentin tilan, kun joitain propseja tai tiloja muutetaan), sinun ei pitäisi tarvita Effektia. Tarpeettomien efektien poistaminen tekee koodistasi helpommin seurattavan, nopeamman suorittaa ja vähemmän virhealttiin.
Tulet oppimaan
- Miksi ja miten poistaa tarpeettomat Effektit komponenteistasi
- Miten välimuistittaa kalliit laskutoimitukset ilman Effekteja
- Miten nollata ja säätää komponentin tilaa ilman Effekteja
- Miten jakaa logiikkaa tapahtumankäsittelijöiden välillä
- Millainen logiikka tulisi siirtää tapahtumankäsittelijöihin
- Miten ilmoittaa muutoksista vanhemmille komponenteille
Miten poistaa turhia Effecteja
On kaksi yleistä tapausta, joissa et tarvitse efektejä:
- Et tarvitse efektejä datan muokkaamiseen renderöintiä varten. Esimerkiksi, sanotaan että haluat suodattaa listaa ennen sen näyttämistä. Saatat tuntea houkutuksen efektin kirjoittamiseen, joka päivittää tilamuuttujan, kun lista muuttuu. Kuitenkin tämä on tehottomaa. Kun päivität tilaa, React ensin kutsuu komponenttifunktioitasi laskemaan, mitä tulisi näytölle. Sitten React “kommittaa” nämä muutokset DOMiin päivittäen näytön. Sitten React suorittaa efektit. Jos efektisi myös päivittää välittömästi tilaa, tämä käynnistää koko prosessin alusta! Välttääksesi tarpeettomat renderöintikierrokset, muokkaa kaikki data komponenttiesi ylätasolla. Tuo koodi ajetaan automaattisesti aina kun propsit tai tila muuttuvat.
- Et tarvitse efektejä käsittelemään käyttäjätapahtumia. Esimerkiksi, oletetaan että haluat lähettää
/api/buy
POST-pyynnön ja näyttää ilmoituksen, kun käyttäjä ostaa tuotteen. Osta-nappulan klikkaustapahtumankäsittelijässä tiedät tarkalleen mitä tapahtui. Kun efekti suoritetaan, et tiedä mitä käyttäjä teki (esimerkiksi, minkä nappulan hän klikkasi). Tämän vuoksi käyttäjätapahtumat käsitellään yleensä vastaavissa tapahtumankäsittelijöissä.
Tarvitset kyllä efektejä synkronoimiseen ulkoisten järjestelmien kanssa. Esimerkiksi voit kirjoittaa efektin, joka pitää jQuery-widgetin synkronoituna Reactin tilan kanssa. Voit myös noutaa tietoja efekteillä: esimerkiksi voit pitää hakutulokset synkronoituna nykyisen hakukyselyn kanssa. On kuitenkin hyvä pitää mielessä, että nykyaikaiset kehysratkaisut tarjoavat tehokkaampia sisäänrakennettuja tiedonhakumekanismeja kuin efektien kirjoittaminen suoraan komponentteihin.
Katsotaanpa joitakin yleisiä konkreettisia esimerkkejä saadaksesi oikeanlaisen intuition.
Tilan päivittäminen propsin tai tilan pohjalta
Oletetaan, että sinulla on komponentti, jossa on kaksi tilamuuttujaa: firstName
ja lastName
. Haluat laskea niistä fullName
-nimen yhdistämällä ne. Lisäksi haluat, että fullName
päivittyy aina, kun firstName
tai lastName
muuttuvat. Ensimmäinen vaistosi saattaa olla lisätä fullName
-tilamuuttuja ja päivittää se effektissa:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Vältä: turha tila ja tarpeeton Effekti
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
Tämä on tarpeettoman monimutkainen. Se on myös tehotonta: se suorittaa koko renderöinnin vanhentuneella fullName
-arvolla ja päivittää sen sitten välittömästi uudelleen päivitetyllä arvolla. Poista tilamuuttuja ja Effekti:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Hyvä: lasketaan renderöinnin aikana
const fullName = firstName + ' ' + lastName;
// ...
}
Kun jotain voidaan laskea olemassa olevista propseista tai tilamuuttujista, älä aseta sitä tilaan. Sen sijaan laske se renderöinnin aikana. Tämä tekee koodistasi nopeamman (vältät ylimääräiset “kaskadiset” päivitykset), yksinkertaisemman (poistat osan koodista) ja vähemmän virhealttiin (vältät bugeja, jotka johtuvat tilamuuttujien epäsynkronoinnista). Jos tämä lähestymistapa tuntuu uudelta sinulle, Ajattelu Reactissa selittää, mitä tilaan tulisi laittaa.
Raskaiden laskujen välimuistittaminen
Tämä komponentti laskee visibleTodos
-muuttujan ottamalla todos
-muuttujan propsina vastaan ja suodattamalla sen filter
-propsin perusteella. Saatat tuntea houkutuksen tallentaa tulos tilaan ja päivittää sen Effektin avulla:
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 Vältä: turha tila ja tarpeeton Effekti
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
Kuten aiemmassa esimerkissä, tämä on sekä tarpeeton että tehoton. Poista ensin tila ja Effekti:
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Tämä on okei jos getFilteredTodos() ei ole hidas.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
Useiten, tämä koodi on okei! Mutta ehkä getFilteredTodos()
on hidas tai sinulla on useita todos
kohteita. Tässä tapauksessa et halua laskea getFilteredTodos()
uudelleen, jos jokin epäolennainen tilamuuttuja, kuten newTodo
, on muuttunut.
Voit välimuistittaa (tai “memoisoida”) kalliin laskutoimituksen käärimällä sen useMemo
-Hookin sisään:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Ei suoriteta uudelleen, elleivät todos tai filter muutu
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
Tai kirjoitettuna yhtenä rivinä:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ getFilteredTodos()-funktiota ei suoriteta uudelleen, elleivät todos tai filter muutu.
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
Tämä kertoo Reactille, että et halua sisäisen funktion suorittuvan uudelleen, elleivät todos
tai filter
ole muuttuneet. React muistaa getFilteredTodos()
-funktion palautusarvon ensimmäisellä renderöinnillä. Seuraavilla renderöinneillä se tarkistaa, ovatko todos
tai filter
erilaisia. Jos ne ovat samat kuin viime kerralla, useMemo
palauttaa viimeksi tallennetun tuloksen. Mutta jos ne ovat erilaisia, React kutsuu sisäistä funktiota uudelleen (ja tallentaa sen tuloksen).
Funktio, jonka käärit useMemo
-Hookin sisään, suoritetaan renderöinnin aikana, joten tämä toimii vain puhtaiden laskutoimitusten kanssa.
Syväsukellus
Yleisesti ottaen, ellet luo tai silmukoi tuhansia objekteja, se ei todennäköisesti ole kallista. Jos haluat olla varmempi, voit lisätä konsolilokin mittaamaan aikaa, joka kuluu koodin palan suorittamiseen:
console.time('filter taulukko');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter taulukko');
Suorita vuorovaikutus, jota mitataan (esimerkiksi kirjoittaminen syötekenttään). Näet sitten lokit, kuten filter taulukko: 0.15ms
konsolissasi. Jos kokonaisaika on merkittävä (esimerkiksi 1ms
tai enemmän), saattaa olla järkevää välimuistittaa laskutoimitus. Kokeilun vuoksi voit sitten kääriä laskutoimituksen useMemo
-Hookin sisään ja tarkistaa, onko kokonaisaika vähentynyt vai ei:
console.time('filter taulukko');
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter); // Ohita, jos todos ja filter eivät ole muuttuneet.
}, [todos, filter]);
console.timeEnd('filter taulukko');
useMemo
ei tee ensimmäistä renderöintiä nopeammaksi. Se auttaa ainoastaan välttämään tarpeetonta työtä päivityksissä.
Pidä mielessä, että koneesi on todennäköisesti nopeampi kuin käyttäjäsi, joten on hyvä idea testata suorituskykyä keinotekoisella hidastuksella. Esimerkiksi Chrome tarjoaa CPU Throttling-vaihtoehdon tätä varten.
Huomaa myös, että suorituskyvyn mittaaminen kehitysvaiheessa ei anna sinulle tarkimpia tuloksia. (Esimerkiksi, kun Strict Mode on päällä, näet jokaisen komponentin renderöityvän kahdesti kerran sijaan.) Saadaksesi tarkimmat ajat, rakenna sovelluksesi tuotantoon ja testaa sitä laitteella, joka käyttäjilläsi on.
Kaiken tilan palauttaminen kun propsi muuttuu
ProfilePage
komponentti saa userId
propsin. Sivulla on kommenttikenttä, ja käytät comment
-tilamuuttujaa sen arvon säilyttämiseen. Eräänä päivänä huomaat ongelman: kun navigoit yhdestä profiilista toiseen, comment
-tila ei nollaudu. Tämän seurauksena on helppo vahingossa lähettää kommentti väärälle käyttäjän profiilille. Korjataksesi ongelman, haluat tyhjentää comment
-tilamuuttujan aina, kun userId
muuttuu:
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Vältä: Tilan resetointi prospin muuttuesssa Effektissa
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
Tämä on tehotonta, koska ProfilePage
ja sen lapset renderöityvät ensin vanhentuneella arvolla ja sitten uudelleen. Se on myös monimutkaista, koska sinun täytyisi tehdä tämä jokaisessa komponentissa, jossa on tilaa ProfilePage
:n sisällä. Esimerkiksi, jos kommenttikäyttöliittymä on sisäkkäinen, haluat nollata myös sisäkkäisen kommentin tilan.
Sen sijaan, voit kertoa Reactille, että jokainen käyttäjän profiili on käsitteellisesti erilainen profiili antamalla sille eksplisiittisen avaimen. Jaa komponenttisi kahteen ja välitä key
-attribuutti ulkoisesta komponentista sisäiseen:
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ Tämä ja muut alla olevat tilat nollautuvat key:n muuttuessa automaattisesti
const [comment, setComment] = useState('');
// ...
}
Normaalisti, React säilyttää tilan kun sama komponentti on renderöity samaan paikkaan. Antamalla userId
:n key
-attribuuttina Profile
-komponentille, pyydät Reactia kohtelemaan kahta Profile
-komponenttia, joilla on eri userId
, kahtena eri komponenttina, jotka eivät jaa tilaa. Aina kun avain (jonka olet asettanut userId
:ksi) muuttuu, React luo uudelleen DOMin ja nollaa tilan Profile
-komponentissa ja kaikissa sen lapsikomponenteissa. Nyt comment
-kenttä tyhjenee automaattisesti navigoidessasi profiilien välillä.
Huomaa, että tässä esimerkissä vain ulkoinen ProfilePage
-komponentti on exportattu ja näkyvissä muissa projektin tiedostoissa. Komponentit, jotka renderöivät ProfilePage
:a, eivät tarvitse välittää avainta sille: ne välittävät userId
:n tavallisena propina. Se, että ProfilePage
välittää sen key
-attribuuttina sisäiselle Profile
-komponentille, on toteutuksen yksityiskohta.
Tilan säätäminen kun propsi muuttuu
Sometimes, you might want to reset or adjust a part of the state on a prop change, but not all of it.
This List
component receives a list of items
as a prop, and maintains the selected item in the selection
state variable. You want to reset the selection
to null
whenever the items
prop receives a different array:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Avoid: Adjusting state on prop change in an Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
This, too, is not ideal. Every time the items
change, the List
and its child components will render with a stale selection
value at first. Then React will update the DOM and run the Effects. Finally, the setSelection(null)
call will cause another re-render of the List
and its child components, restarting this whole process again.
Start by deleting the Effect. Instead, adjust the state directly during rendering:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Better: Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
Storing information from previous renders like this can be hard to understand, but it’s better than updating the same state in an Effect. In the above example, setSelection
is called directly during a render. React will re-render the List
immediately after it exits with a return
statement. React has not rendered the List
children or updated the DOM yet, so this lets the List
children skip rendering the stale selection
value.
When you update a component during rendering, React throws away the returned JSX and immediately retries rendering. To avoid very slow cascading retries, React only lets you update the same component’s state during a render. If you update another component’s state during a render, you’ll see an error. A condition like items !== prevItems
is necessary to avoid loops. You may adjust state like this, but any other side effects (like changing the DOM or setting timeouts) should stay in event handlers or Effects to keep components pure.
Although this pattern is more efficient than an Effect, most components shouldn’t need it either. No matter how you do it, adjusting state based on props or other state makes your data flow more difficult to understand and debug. Always check whether you can reset all state with a key or calculate everything during rendering instead. For example, instead of storing (and resetting) the selected item, you can store the selected item ID:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: Calculate everything during rendering
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
Now there is no need to “adjust” the state at all. If the item with the selected ID is in the list, it remains selected. If it’s not, the selection
calculated during rendering will be null
because no matching item was found. This behavior is different, but arguably better because most changes to items
preserve the selection.
Logiikan jakaminen tapahtumakäsittelijöiden kesken
Let’s say you have a product page with two buttons (Buy and Checkout) that both let you buy that product. You want to show a notification whenever the user puts the product in the cart. Calling showNotification()
in both buttons’ click handlers feels repetitive so you might be tempted to place this logic in an Effect:
function ProductPage({ product, addToCart }) {
// 🔴 Avoid: Event-specific logic inside an Effect
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
This Effect is unnecessary. It will also most likely cause bugs. For example, let’s say that your app “remembers” the shopping cart between the page reloads. If you add a product to the cart once and refresh the page, the notification will appear again. It will keep appearing every time you refresh that product’s page. This is because product.isInCart
will already be true
on the page load, so the Effect above will call showNotification()
.
When you’re not sure whether some code should be in an Effect or in an event handler, ask yourself why this code needs to run. Use Effects only for code that should run because the component was displayed to the user. In this example, the notification should appear because the user pressed the button, not because the page was displayed! Delete the Effect and put the shared logic into a function called from both event handlers:
function ProductPage({ product, addToCart }) {
// ✅ Good: Event-specific logic is called from event handlers
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
This both removes the unnecessary Effect and fixes the bug.
POST pyynnön lähettäminen
This Form
component sends two kinds of POST requests. It sends an analytics event when it mounts. When you fill in the form and click the Submit button, it will send a POST request to the /api/register
endpoint:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic should run because the component was displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 Avoid: Event-specific logic inside an Effect
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}
Let’s apply the same criteria as in the example before.
The analytics POST request should remain in an Effect. This is because the reason to send the analytics event is that the form was displayed. (It would fire twice in development, but see here for how to deal with that.)
However, the /api/register
POST request is not caused by the form being displayed. You only want to send the request at one specific moment in time: when the user presses the button. It should only ever happen on that particular interaction. Delete the second Effect and move that POST request into the event handler:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic runs because the component was displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
function handleSubmit(e) {
e.preventDefault();
// ✅ Good: Event-specific logic is in the event handler
post('/api/register', { firstName, lastName });
}
// ...
}
When you choose whether to put some logic into an event handler or an Effect, the main question you need to answer is what kind of logic it is from the user’s perspective. If this logic is caused by a particular interaction, keep it in the event handler. If it’s caused by the user seeing the component on the screen, keep it in the Effect.
Laskutoimitusten ketjutus
Sometimes you might feel tempted to chain Effects that each adjust a piece of state based on other state:
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
// ...
There are two problems with this code.
One problem is that it is very inefficient: the component (and its children) have to re-render between each set
call in the chain. In the example above, in the worst case (setCard
→ render → setGoldCardCount
→ render → setRound
→ render → setIsGameOver
→ render) there are three unnecessary re-renders of the tree below.
Even if it weren’t slow, as your code evolves, you will run into cases where the “chain” you wrote doesn’t fit the new requirements. Imagine you are adding a way to step through the history of the game moves. You’d do it by updating each state variable to a value from the past. However, setting the card
state to a value from the past would trigger the Effect chain again and change the data you’re showing. Such code is often rigid and fragile.
In this case, it’s better to calculate what you can during rendering, and adjust the state in the event handler:
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ Calculate what you can during rendering
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ Calculate all the next state in the event handler
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ...
This is a lot more efficient. Also, if you implement a way to view game history, now you will be able to set each state variable to a move from the past without triggering the Effect chain that adjusts every other value. If you need to reuse logic between several event handlers, you can extract a function and call it from those handlers.
Remember that inside event handlers, state behaves like a snapshot. For example, even after you call setRound(round + 1)
, the round
variable will reflect the value at the time the user clicked the button. If you need to use the next value for calculations, define it manually like const nextRound = round + 1
.
In some cases, you can’t calculate the next state directly in the event handler. For example, imagine a form with multiple dropdowns where the options of the next dropdown depend on the selected value of the previous dropdown. Then, a chain of Effects is appropriate because you are synchronizing with network.
Sovelluksen alustaminen
Some logic should only run once when the app loads.
You might be tempted to place it in an Effect in the top-level component:
function App() {
// 🔴 Avoid: Effects with logic that should only ever run once
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}
However, you’ll quickly discover that it runs twice in development. This can cause issues—for example, maybe it invalidates the authentication token because the function wasn’t designed to be called twice. In general, your components should be resilient to being remounted. This includes your top-level App
component.
Although it may not ever get remounted in practice in production, following the same constraints in all components makes it easier to move and reuse code. If some logic must run once per app load rather than once per component mount, add a top-level variable to track whether it has already executed:
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
You can also run it during module initialization and before the app renders:
if (typeof window !== 'undefined') { // Check if we're running in the browser.
// ✅ Only runs once per app load
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
Code at the top level runs once when your component is imported—even if it doesn’t end up being rendered. To avoid slowdown or surprising behavior when importing arbitrary components, don’t overuse this pattern. Keep app-wide initialization logic to root component modules like App.js
or in your application’s entry point.
Tilamuutosten ilmoittaminen pääkomponentille
Let’s say you’re writing a Toggle
component with an internal isOn
state which can be either true
or false
. There are a few different ways to toggle it (by clicking or dragging). You want to notify the parent component whenever the Toggle
internal state changes, so you expose an onChange
event and call it from an Effect:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
// 🔴 Avoid: The onChange handler runs too late
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])
function handleClick() {
setIsOn(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}
// ...
}
Like earlier, this is not ideal. The Toggle
updates its state first, and React updates the screen. Then React runs the Effect, which calls the onChange
function passed from a parent component. Now the parent component will update its own state, starting another render pass. It would be better to do everything in a single pass.
Delete the Effect and instead update the state of both components within the same event handler:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
// ✅ Good: Perform all updates during the event that caused them
setIsOn(nextIsOn);
onChange(nextIsOn);
}
function handleClick() {
updateToggle(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}
// ...
}
With this approach, both the Toggle
component and its parent component update their state during the event. React batches updates from different components together, so there will only be one render pass.
You might also be able to remove the state altogether, and instead receive isOn
from the parent component:
// ✅ Also good: the component is fully controlled by its parent
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}
// ...
}
“Lifting state up” lets the parent component fully control the Toggle
by toggling the parent’s own state. This means the parent component will have to contain more logic, but there will be less state overall to worry about. Whenever you try to keep two different state variables synchronized, try lifting state up instead!
Tiedon välittäminen pääkomponentille
This Child
component fetches some data and then passes it to the Parent
component in an Effect:
function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}
function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 Avoid: Passing data to the parent in an Effect
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}
In React, data flows from the parent components to their children. When you see something wrong on the screen, you can trace where the information comes from by going up the component chain until you find which component passes the wrong prop or has the wrong state. When child components update the state of their parent components in Effects, the data flow becomes very difficult to trace. Since both the child and the parent need the same data, let the parent component fetch that data, and pass it down to the child instead:
function Parent() {
const data = useSomeAPI();
// ...
// ✅ Good: Passing data down to the child
return <Child data={data} />;
}
function Child({ data }) {
// ...
}
This is simpler and keeps the data flow predictable: the data flows down from the parent to the child.
Tilaaminen ulkoiseen varastoon
Sometimes, your components may need to subscribe to some data outside of the React state. This data could be from a third-party library or a built-in browser API. Since this data can change without React’s knowledge, you need to manually subscribe your components to it. This is often done with an Effect, for example:
function useOnlineStatus() {
// Not ideal: Manual store subscription in an Effect
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
Here, the component subscribes to an external data store (in this case, the browser navigator.onLine
API). Since this API does not exist on the server (so it can’t be used for the initial HTML), initially the state is set to true
. Whenever the value of that data store changes in the browser, the component updates its state.
Although it’s common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead. Delete the Effect and replace it with a call to useSyncExternalStore
:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
// ✅ Good: Subscribing to an external store with a built-in Hook
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
This approach is less error-prone than manually syncing mutable data to React state with an Effect. Typically, you’ll write a custom Hook like useOnlineStatus()
above so that you don’t need to repeat this code in the individual components. Read more about subscribing to external stores from React components.
Tiedon haku
Many apps use Effects to kick off data fetching. It is quite common to write a data fetching Effect like this:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 🔴 Avoid: Fetching without cleanup logic
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
You don’t need to move this fetch to an event handler.
This might seem like a contradiction with the earlier examples where you needed to put the logic into the event handlers! However, consider that it’s not the typing event that’s the main reason to fetch. Search inputs are often prepopulated from the URL, and the user might navigate Back and Forward without touching the input.
It doesn’t matter where page
and query
come from. While this component is visible, you want to keep results
synchronized with data from the network for the current page
and query
. This is why it’s an Effect.
However, the code above has a bug. Imagine you type "hello"
fast. Then the query
will change from "h"
, to "he"
, "hel"
, "hell"
, and "hello"
. This will kick off separate fetches, but there is no guarantee about which order the responses will arrive in. For example, the "hell"
response may arrive after the "hello"
response. Since it will call setResults()
last, you will be displaying the wrong search results. This is called a “race condition”: two different requests “raced” against each other and came in a different order than you expected.
To fix the race condition, you need to add a cleanup function to ignore stale responses:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
This ensures that when your Effect fetches data, all responses except the last requested one will be ignored.
Handling race conditions is not the only difficulty with implementing data fetching. You might also want to think about caching responses (so that the user can click Back and see the previous screen instantly), how to fetch data on the server (so that the initial server-rendered HTML contains the fetched content instead of a spinner), and how to avoid network waterfalls (so that a child can fetch data without waiting for every parent).
These issues apply to any UI library, not just React. Solving them is not trivial, which is why modern frameworks provide more efficient built-in data fetching mechanisms than fetching data in Effects.
If you don’t use a framework (and don’t want to build your own) but would like to make data fetching from Effects more ergonomic, consider extracting your fetching logic into a custom Hook like in this example:
function SearchResults({ query }) {
const [page, setPage] = useState(1);
const params = new URLSearchParams({ query, page });
const results = useData(`/api/search?${params}`);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}
You’ll likely also want to add some logic for error handling and to track whether the content is loading. You can build a Hook like this yourself or use one of the many solutions already available in the React ecosystem. Although this alone won’t be as efficient as using a framework’s built-in data fetching mechanism, moving the data fetching logic into a custom Hook will make it easier to adopt an efficient data fetching strategy later.
In general, whenever you have to resort to writing Effects, keep an eye out for when you can extract a piece of functionality into a custom Hook with a more declarative and purpose-built API like useData
above. The fewer raw useEffect
calls you have in your components, the easier you will find to maintain your application.
Kertaus
- If you can calculate something during render, you don’t need an Effect.
- To cache expensive calculations, add
useMemo
instead ofuseEffect
. - To reset the state of an entire component tree, pass a different
key
to it. - To reset a particular bit of state in response to a prop change, set it during rendering.
- Code that runs because a component was displayed should be in Effects, the rest should be in events.
- If you need to update the state of several components, it’s better to do it during a single event.
- Whenever you try to synchronize state variables in different components, consider lifting state up.
- You can fetch data with Effects, but you need to implement cleanup to avoid race conditions.
Haaste 1 / 4: Transform data without Effects
The TodoList
below displays a list of todos. When the “Show only active todos” checkbox is ticked, completed todos are not displayed in the list. Regardless of which todos are visible, the footer displays the count of todos that are not yet completed.
Simplify this component by removing all the unnecessary state and Effects.
import { useState, useEffect } from 'react'; import { initialTodos, createTodo } from './todos.js'; export default function TodoList() { const [todos, setTodos] = useState(initialTodos); const [showActive, setShowActive] = useState(false); const [activeTodos, setActiveTodos] = useState([]); const [visibleTodos, setVisibleTodos] = useState([]); const [footer, setFooter] = useState(null); useEffect(() => { setActiveTodos(todos.filter(todo => !todo.completed)); }, [todos]); useEffect(() => { setVisibleTodos(showActive ? activeTodos : todos); }, [showActive, todos, activeTodos]); useEffect(() => { setFooter( <footer> {activeTodos.length} todos left </footer> ); }, [activeTodos]); return ( <> <label> <input type="checkbox" checked={showActive} onChange={e => setShowActive(e.target.checked)} /> Show only active todos </label> <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} /> <ul> {visibleTodos.map(todo => ( <li key={todo.id}> {todo.completed ? <s>{todo.text}</s> : todo.text} </li> ))} </ul> {footer} </> ); } function NewTodo({ onAdd }) { const [text, setText] = useState(''); function handleAddClick() { setText(''); onAdd(createTodo(text)); } return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={handleAddClick}> Add </button> </> ); }