Anda Mungkin Tidak Membutuhkan Effect

Effects adalah jalan keluar dari paradigma React. Mereka memungkinkan Anda untuk “keluar” dari React dan menyinkronkan komponen Anda dengan sistem eksternal. Jika tidak ada sistem eksternal yang terlibat (misalkan, jika Anda ingin memperbarui state komponen dengan beberapa props atau perubahan state), Anda seharusnya tidak perlu menggunakan Effect. Menghilangkan Effects yang tidak perlu akan membuat kode Anda lebih mudah untuk diikuti, lebih cepat untuk dijalankan, dan lebih sedikit berpotensi galat.

Anda akan mempelajari

  • Mengapa dan cara menghapus Effects yang tidak perlu dari komponen Anda
  • Cara meng-cache komputasi yang mahal tanpa Effects
  • Cara menyetel ulang dan mengatur state komponen tanpa Effects
  • Cara berbagi logika di antara event handler share logic between event handlers
  • Logika apa yang seharusnya dipindahkan ke event handler
  • Cara memberi tahu perubahan komponen ke komponen induk

Cara menghapus Effect yang tidak perlu

Ada dua kasus umum di mana Anda tidak memerlukan Effects:

  • Anda tidak memerlukan Effects untuk melakukan transformasi data untuk rendering. Sebagai contoh, katakanlah Anda ingin melakukan filter terhadap sebuah daftar sebelum menampiklannya. Anda mungkin merasa tergoda untuk menulis Effect yang memperbarui variabel state ketika daftar berubah. Akan tetapi, hal ini tidak efisien. Ketika Anda memperbarui state, React akan memanggil fungsi komponen Anda terlebih dahulu untuk menghitung apa yang seharusnya ada di layar. Kemudian React akan commit perubahan ini ke DOM, memperbarui layar. Kemudian React akan menjalankan Effect Anda. Jika Effect Anda juga segera memperbarui state, ini akan mengulang seluruh proses dari awal! Untuk menghindari render pass yang tidak perlu, ubah semua data pada tingkat teratas komponen Anda. Kode tersebut akan secara otomatis dijalankan ulang setiap kali props atau state anda berubah.
  • Anda tidak memerlukan Efek untuk menangani event dari pengguna. Sebagai contoh, katakanlah Anda ingin mengirim request POST /api/buy dan menampilkan notifikasi ketika pengguna membeli produk. Di event handler klik tombol Beli, Anda tahu persis apa yang terjadi. Pada saat Effect berjalan, Anda tidak tahu apa yang dilakukan pengguna (misalnya, tombol mana yang diklik). Inilah sebabnya mengapa Anda biasanya akan menangani event pengguna di event handler yang sesuai.

Anda memang membutuhkan Effect untuk melakukan sinkronisasi dengan sistem eksternal. dengan sistem eksternal. Sebagai contoh, Anda dapat menulis sebuah Effect yang membuat widget jQuery tetap tersinkronisasi dengan state React. Anda juga dapat mengambil data dengan Effect: sebagai contoh, Anda dapat menyinkronkan hasil pencarian dengan kueri pencarian saat ini. Perlu diingat bahwa kerangka kerja (framework) modern menyediakan mekanisme pengambilan data bawaan yang lebih efisien daripada menulis Effect secara langsung di dalam komponen Anda.

Untuk membantu Anda mendapatkan intuisi yang tepat, mari kita lihat beberapa contoh konkret yang umum!

Memperbarui state berdasarkan props atau state

Katakanlah Anda memiliki komponen dengan dua variabel state: firstName dan lastName. Anda ingin mendapatkan fullName dengan menggabungkan keduanya. Selain itu, Anda ingin fullName diperbarui setiap kali firstName atau lastName berubah. Naluri pertama Anda mungkin menambahkan variabel state fullName dan memperbaruinya di Effect:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');

// 🔴 Hindari: state berlebihan dan Effect yang tidak perlu
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}

Ini lebih rumit dari yang diperlukan. Ini juga tidak efisien: ia melakukan render pass secara keseluruhan dengan nilai usang untuk fullName, lalu segera me-render ulang dengan nilai yang diperbarui. Hapus variabel state dan Effect:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Baik: dikalkulasi saat render
const fullName = firstName + ' ' + lastName;
// ...
}

Ketika sebuah nilai dapat dihitung dari props atau state yang ada, jangan memasukkannya ke dalam state. Sebaiknya, hitunglah saat rendering. Hal ini membuat kode Anda lebih cepat (Anda menghindari pembaruan “bertingkat” tambahan), lebih sederhana (Anda menghapus beberapa kode), dan lebih tidak rawan terhadap error (Anda menghindari bug yang disebabkan oleh variabel state berbeda yang tidak sinkron satu sama lain). Jika pendekatan ini terasa baru bagi Anda, Cara Berpikir dengan React menjelaskan apa yang seharusnya masuk sebagai state.

Menyimpan penghitungan mahal di cache

Komponen ini menghitung visibleTodos dengan mengambil todos yang diterimanya berdasarkan props dan memfilternya berdasarkan prop filter. Anda mungkin tergoda untuk menyimpan hasilnya dalam keadaan dan memperbaruinya dari Effect:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');

// 🔴 Hindari: state berlebihan dan Effect yang tidak perlu
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

// ...
}

Seperti pada contoh sebelumnya, hal ini tidak diperlukan dan tidak efisien. Pertama, hapus state dan Effect-nya:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Ini baik jika getFilteredTodos() tidak lambat.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}

Biasanya, kode ini baik-baik saja! Namun mungkin getFilteredTodos() lambat atau Anda memiliki banyak todos. Dalam hal ini Anda tidak ingin mengkalkulasi ulang getFilteredTodos() jika beberapa variabel state yang tidak terkait seperti newTodo telah berubah.

Anda dapat melakukan cache (atau “memoisasi”) perhitungan yang mahal dengan membungkusnya dalam Hook useMemo:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Tidak dijalankan ulang kecuali todos atau filter berubah
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}

Atau, ditulis dalam satu baris:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Does not re-run getFilteredTodos() unless todos or filter change
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}

Hal ini memberitahu React bahwa Anda tidak ingin fungsi di dalamnya dijalankan ulang kecuali todos atau filter telah berubah. React akan mengingat nilai kembalian getFilteredTodos() selama render awal. Selama rendering berikutnya, ia akan memeriksa apakah todos atau filter berbeda. Jika sama dengan yang terakhir kali, useMemo akan mengembalikan hasil terakhir yang disimpannya. Namun jika berbeda, React akan memanggil fungsi di dalamnya lagi (dan menyimpan hasilnya).

Fungsi yang Anda bungkus dalam useMemo berjalan selama rendering, jadi ini hanya berfungsi untuk perhitungan murni.

Pendalaman

Bagaimana cara mengetahui apakah suatu perhitungan itu mahal?

Secara umum, kecuali Anda membuat atau melakukan pengulangan terhadap ribuan objek, biayanya mungkin tidak mahal. Jika Anda ingin lebih percaya diri, Anda dapat menambahkan log konsol untuk mengukur waktu yang dihabiskan dalam sebuah kode:

console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');

Lakukan interaksi yang Anda ukur (misalnya, mengetik pada input). Anda kemudian akan melihat log seperti filter array: 0,15ms di konsol Anda. Jika keseluruhan waktu yang dicatat bertambah hingga jumlah yang signifikan (katakanlah, 1 ms atau lebih), mungkin masuk akal untuk melakukan memoisasi terhadap penghitungan tersebut. Sebagai percobaan, Anda kemudian dapat menggabungkan penghitungan dalam useMemo untuk memverifikasi apakah total waktu yang dicatat untuk interaksi tersebut telah berkurang atau tidak:

console.time('filter array');
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter); // Skipped if todos and filter haven't changed
}, [todos, filter]);
console.timeEnd('filter array');

useMemo tidak akan membuat rendering pertama lebih cepat. Ini hanya membantu Anda melewatkan pekerjaan pembaruan yang tidak perlu.

Ingatlah bahwa perangkat Anda mungkin lebih cepat daripada perangkat pengguna Anda, jadi sebaiknya uji kinerjanya dengan pelambatan buatan. Misalnya, Chrome menawarkan opsi CPU Throttling untuk ini.

Perhatikan juga bahwa mengukur kinerja dalam mode pengembangan tidak akan memberi Anda hasil yang paling akurat. (Misalnya, ketika Strict Mode aktif, Anda akan melihat setiap komponen dirender dua kali, bukan sekali.) Untuk mendapatkan pengaturan waktu yang paling akurat, kompilasi aplikasi Anda dalam mode produksi dan uji pada perangkat seperti yang dimiliki pengguna Anda.

Menyetel ulang keseluruhan state ketika props berubah

Komponen ProfilePage ini menerima prop userId. Halaman tersebut berisi input komentar, dan Anda menggunakan variabel state comment untuk menyimpan nilainya. Suatu hari, Anda melihat masalah: saat Anda bernavigasi dari satu profil ke profil lainnya, state comment tidak disetel ulang. Akibatnya, Anda dapat dengan mudah mengirim komentar ke profil pengguna yang salah secara tidak sengaja. Untuk memperbaiki masalah ini, Anda ingin menghapus variabel state comment setiap kali userId berubah:

export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');

// 🔴 Hindari: menyetel ulanh state setiap prop berubah di dalam Effect
useEffect(() => {
setComment('');
}, [userId]);
// ...
}

Hal ini tidak efisien karena ProfilePage dan turunannya akan di-render terlebih dahulu dengan nilai yang sudah usang, lalu di-render lagi. Ini juga rumit karena Anda harus melakukan ini di setiap komponen yang memiliki state di dalam ProfilePage. Misalnya, jika UI komentar disarangkan, Anda juga ingin menghapus state komentar yang disarangkan.

Sebagai gantinya, Anda dapat memberi tahu React bahwa setiap profil pengguna secara konseptual adalah profil berbeda dengan memberinya key secara eksplisit. Pisahkan komponen Anda menjadi dua dan oper atribut key dari komponen luar ke komponen dalam:

export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}

function Profile({ userId }) {
// ✅ State ini dan state lain di bawahnya akan disetel ulang secara otomatis setiap kali key berubah
const [comment, setComment] = useState('');
// ...
}

Biasanya, React mempertahankan state ketika komponen yang sama dirender di tempat yang sama. Dengan mengoper userId sebagai key ke komponen Profile, Anda meminta React untuk memperlakukan dua komponen Profile dengan userId yang berbeda sebagai dua komponen berbeda yang tidak boleh berbagi state apa pun. Kapan pun key (yang telah Anda setel ke userId) berubah, React akan membuat ulang DOM dan mengatur ulang state dari komponen Profile dan semua turunannya. Sekarang bidang comment akan dihapus secara otomatis saat bernavigasi antar profil.

Perhatikan bahwa dalam contoh ini, hanya komponen ProfilePage bagian luar yang diekspor dan terlihat oleh file lain dalam proyek. Komponen yang merender ProfilePage tidak perlu meneruskan kuncinya: komponen meneruskan userId sebagai prop biasa. Fakta bahwa ProfilePage meneruskannya sebagai key ke komponen Profile bagian dalam adalah detail implementasi.

Menyesuaikan sebagian state ketika prop berubah

Terkadang, Anda mungkin ingin menyetel ulang atau menyesuaikan sebagian state pada perubahan prop, namun tidak semuanya.

Komponen List ini menerima list item sebagai prop, dan mempertahankan item yang dipilih dalam variabel state selection. Anda ingin menyetel ulang selection ke null setiap kali prop items menerima senarai yang berbeda:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 🔴 Hindari: Mengatur state saat prop berubah di dalam Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}

Hal ini juga tidak ideal. Setiap kali items berubah, List dan komponen turunannya akan di-render dengan nilai selection yang usang pada awalnya. Kemudian React akan memperbarui DOM dan menjalankan Effect-nya. Terakhir, panggilan setSelection(null) akan menyebabkan rendering ulang List dan komponen turunannya lagi, sehingga memulai kembali seluruh proses ini.

Mulailah dengan menghapus Effect. Sebagai gantinya, sesuaikan state secara langsung selama rendering:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// Lebih baik: Menyesuaikan state saat rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}

Menyimpan informasi dari render sebelumnya seperti ini mungkin sulit untuk dipahami, tetapi ini lebih baik daripada memperbarui state yang sama dalam suatu Effect. Dalam contoh di atas, setSelection dipanggil secara langsung saat render. React akan me-render ulang List segera setelah keluar dengan pernyataan return. React belum merender turunan List atau memperbarui DOM, jadi hal ini memungkinkan turunan List melewatkan rendering nilai selection yang sudah usang.

Saat Anda memperbarui komponen selama rendering, React membuang JSX yang dikembalikan dan segera mencoba lagi rendering. Untuk menghindari percobaan ulang berjenjang yang sangat lambat, React hanya mengizinkan Anda memperbarui state komponen sama selama render. Jika Anda memperbarui state komponen lain selama render, Anda akan melihat error. Kondisi seperti items !== prevItems diperlukan untuk menghindari perulangan. Anda dapat menyesuaikan state seperti ini, namun efek samping lainnya (seperti mengubah DOM atau menyetel batas waktu) harus tetap berada di event handlers atau Effect untuk menjaga komponen tetap murni.

Meskipun pola ini lebih efisien daripada Effect, sebagian besar komponen juga tidak memerlukannya. Bagaimana pun Anda melakukannya, menyesuaikan state berdasarkan props atau state lainnya akan membuat aliran data Anda lebih sulit untuk dipahami dan di-debug. Selalu periksa apakah Anda dapat mengatur ulang semua state dengan key atau menghitung semuanya selama rendering sebagai gantinya. Misalnya, alih-alih menyimpan (dan mengatur ulang) item yang dipilih, Anda dapat menyimpan ID item yang dipilih:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Cara terbaik: Menghitung semuanya saat rendering
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}

Sekarang tidak perlu lagi “menyesuaikan” state. Jika item dengan ID yang dipilih ada dalam daftar, maka item tersebut tetap dipilih. Jika tidak, selection yang dihitung selama rendering akan menjadi null karena tidak ditemukan item yang cocok. Perilaku ini berbeda, namun bisa dibilang lebih baik karena sebagian besar perubahan pada item mempertahankan pilihan.

Sharing logic between event handlers

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.

Sending a POST request

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.

Chains of computations

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.

Initializing the application

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.

Notifying parent components about state changes

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!

Passing data to the parent

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.

Subscribing to an external store

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.

Fetching data

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.

Rekap

  • If you can calculate something during render, you don’t need an Effect.
  • To cache expensive calculations, add useMemo instead of useEffect.
  • 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.

Tantangan 1 dari 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>
    </>
  );
}