Learn ReactJS - Effects & Lifecycle with Hooks
Episode 6 of 24

Learn ReactJS - Effects & Lifecycle with Hooks

This episode dissects useEffect for side effects, effect cleanup, and the dependency array that controls when effects run. You'll also learn about useMemo and useCallback for optimization, and write custom hooks so logic can be reused across components.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Pure rendering isn't enough for a real app. You need to load data, listen to global events, manage timers, or change the document title — all of these are side effects. In modern React, the lifecycle is managed with hooks, and the star of the show is useEffect.

Episode 6 dissects useEffect thoroughly: when it runs, how to clean up effects, and the role of the dependency array. You'll also look at useMemo and useCallback to avoid unnecessary work and renders, then learn to write custom hooks, which are React's main superpower.

useEffect for Side Effects

An Effect That Runs the First Time

useEffect accepts a function that React runs after committing to the DOM. Without a dependency array, the effect runs after every render:

JSA basic effect
import { useEffect } from "react"
 
function App() {
  useEffect(() => {
    console.log("Komponen dirender")
  })
 
  return <h1>Halo</h1>
}

The effect above runs on every render. For most cases, you want an effect to run only when a specific value changes — that's the job of the dependency array in the next section.

Effect Cleanup and the Dependency Array

The Dependency Array Controls When an Effect Runs

The second array of useEffect contains the values that trigger the effect. Its three main forms:

JSDependency array variations
useEffect(() => { /* sekali, saat mount */ }, [])
 
useEffect(() => { /* saat userId berubah */ }, [userId])
 
useEffect(() => { /* setiap render */ })

useEffect(fn, []) runs once when the component is first mounted — the replacement for componentDidMount. With [userId], the effect runs when userId changes. Without an array, it runs on every render.

Cleanup to Prevent Leaks

Effects like subscriptions or timers must be cleaned up. The function returned from an effect is its cleanup:

JSA timer with cleanup
import { useEffect, useState } from "react"
 
function Jam() {
  const [detik, setDetik] = useState(0)
 
  useEffect(() => {
    const timer = setInterval(() => setDetik((d) => d + 1), 1000)
    return () => clearInterval(timer)
  }, [])
 
  return <p>Sudah {detik} detik</p>
}

return () => clearInterval(timer) is called by React when the component unmounts or before the effect runs again. Without cleanup, the interval keeps running and causes a memory leak.

useMemo and useCallback for Optimization

useMemo for Expensive Computations

useMemo caches a computation result and only recomputes it when a dependency changes:

JSuseMemo for large data
import { useMemo } from "react"
 
function Daftar({ itemList, filter }) {
  const hasil = useMemo(() => {
    return itemList.filter((item) => item.nama.includes(filter))
  }, [itemList, filter])
 
  return <ul>{hasil.map((item) => <li key={item.id}>{item.nama}</li>)}</ul>
}

useMemo(fn, [itemList, filter]) caches the filter result and only recomputes it if itemList or filter changes. For lists of hundreds of thousands of items, this saves significant render time.

useCallback for Stable Functions

useCallback caches a function reference so it stays the same across renders, preventing unnecessary re-renders of child components:

JSuseCallback for handlers
import { useCallback, useState } from "react"
 
function Form() {
  const [teks, setTeks] = useState("")
 
  const handleChange = useCallback((e) => {
    setTeks(e.target.value)
  }, [])
 
  return <input value={teks} onChange={handleChange} />
}

const handleChange = useCallback(fn, []) gives this handler the same reference on every render, which is useful when it's passed to a memoized child component. Remember: use it only when needed — premature optimization adds complexity.

Custom Hooks for Reusable Logic

Why Custom Hooks?

Custom hooks are regular functions starting with use that use other hooks. This is the best way to extract shared logic from many components:

JSA simple custom hook
import { useEffect, useState } from "react"
 
function useLocalStorage(key, awal) {
  const [nilai, setNilai] = useState(() => {
    return localStorage.getItem(key) ?? awal
  })
 
  useEffect(() => {
    localStorage.setItem(key, nilai)
  }, [key, nilai])
 
  return [nilai, setNilai]
}
 
function App() {
  const [nama, setNama] = useLocalStorage("nama", "")
  return <input value={nama} onChange={(e) => setNama(e.target.value)} />
}

useLocalStorage(key, awal) combines useState and useEffect so that storage into localStorage happens automatically. Important rule: call hooks at the top level of a function, without conditions or loops, and without changing the order of calls between renders.

The Rules of Hooks

There are two rules enforced by ESLint (the react-hooks plugin): hooks are only called from React functions (components or custom hooks), and the order of calls must be consistent. Violating these rules produces messy state — so run lint after writing custom hooks.

Check hook rules with lint
npm run lint

If the react-hooks plugin is installed, ESLint automatically detects violations like hooks inside conditions. Keep lint green to avoid subtle bugs.

Conclusion

Episode 6 completed your hooks toolkit: useEffect for side effects with the dependency array and cleanup, useMemo and useCallback for optimization, and custom hooks that make logic reusable across components.

Key takeaways:

  • useEffect runs after commit; the dependency array controls when it runs.
  • Always provide cleanup for timers, subscriptions, and event listeners.
  • useMemo caches computation results; useCallback caches function references.
  • Optimize only when needed: start with clean code, then measure.
  • Custom hooks start with use and extract logic for reuse.
  • Call hooks unconditionally and in a consistent order.

In the next episode, episode 7, we'll cover conditional rendering & lists — ternary and logical operators, rendering lists with stable keys, Fragments, portals, and error boundaries, plus best practices for dynamic UI. It's time to make your UI truly branch and repeat.

Learn ReactJS - Effects & Lifecycle with Hooks | Learn ReactJS