Omid Sayfun
Omid SayfunComputer Geek
Home
Notebook
Journey

Online

Github
Linkedin
Hevy
Plyatomic
Notebook
The trap of making everything dynamic
March 01, 2024
A try at type-safe groupBy function in TypeScript
April 10, 2025
Email special headers
November 20, 2024
Adding prettier to eslint
April 10, 2025
Storing Vector in Postgres with Drizzle ORM
March 21, 2024
Upgrading my blog to Next 15
April 05, 2025
Canvas macOS issue
February 20, 2024
tsx doesn’t support decorators
March 26, 2025
Validating NestJS env vars with zod
February 06, 2025
Extending Window: types vs interfaces
March 21, 2025
Loading env file into Node process
February 06, 2025
Add update date field to Postgres
February 27, 2024
Using node API for delay
February 06, 2025
React Component Lifecycle
November 28, 2024
How CQRS is different than Event Sourcing
August 18, 2024
RabbitMQ exchange vs queue
August 14, 2024
PgVector similarity search distance functions
August 13, 2024
PgVector indexing options for vector similarity search
July 31, 2024
Using puppeteer executable for GSTS
June 08, 2024
Why EQ is Your Next Career Upgrade
May 13, 2024
Counting GPT tokens
June 30, 2024
Logging route handler responses in Next.js 14
June 19, 2024
Redirect www subdomain with Cloudflare
June 17, 2024
Logging requests in Express app
June 16, 2024
Move Docker volume to bind mount
June 12, 2024
Next.js Hydration Window Issue
May 29, 2024
Using Git rebase without creating chaos in your repo
May 16, 2024
Implementing RPC Calls with RabbitMQ in TypeScript
March 16, 2024
Optimize webpage load with special tags
March 15, 2024
What the hell is Open Graph?
March 13, 2024
My go-to Next.js ESlint config
March 10, 2024
List of useful Chrome args
March 10, 2024
Combining RxJS observables - Part 1
February 20, 2024

React Component Lifecycle

November 28, 2024 · Updated on November 29, 2024

Today, I was deep into a React project that relied on user scroll and dynamic component height changes to display a scroll indicator. Sounds straightforward, right? Yeah... no. Things spiraled into chaos pretty quickly, and I found myself brushing up on the React component lifecycle like it was my first day coding.

The super-simplified version of React’s lifecycle is:

  • Mount: Your component makes its grand debut on the page.
  • Update: Props or state change, and React works its magic to reflect that.
  • Unmount: The component leaves the stage.

Behind the scenes, React uses the virtual DOM to track changes and only updates the actual DOM when necessary. Neat, right? But understanding when and how to hook into these lifecycle phases is where things get tricky.

Enter useEffect and useLayoutEffect

I was already familiar with useEffect for tracking changes or handling mount/unmount behavior. Then there’s useLayoutEffect, which executes its logic after React has worked on the virtual DOM but before the browser paints anything on the screen. It’s perfect for layout calculations that need to happen ASAP.

But here’s where things got interesting: What happens when you use these hooks without a dependency array—or with an empty one?

Synchronous vs. Asynchronous: The Key Difference

One thing I had to clarify for myself was the key difference in timing between useEffect and useLayoutEffect:

  • useEffect is asynchronous: It runs after the browser has painted the DOM. This means the user can see updates on the screen before the code inside useEffect executes. It’s perfect for non-blocking operations like API calls or logging.
  • useLayoutEffect is synchronous: It blocks the browser from painting until its code runs. This makes it ideal for tasks like measuring DOM elements or making layout adjustments because you can ensure everything is in place before the user sees it.

Here’s a mental model:

  • useLayoutEffect feels like the backstage crew adjusting the props before the curtains rise.
  • useEffect is the post-show cleanup crew tidying up after the audience has already seen the stage.

Continue Reading

When working with components that depend on precise measurements or UI tweaks, this distinction is huge.

The Experiment

Take this component as an example:

const Component = () => {
	const [count, setCount] = useState(0);
 
	useEffect(() => {
		console.log("UseEffect with empty dependency array");
	}, []);
 
	useLayoutEffect(() => {
		console.log("UseLayoutEffect with empty dependency array");
	}, []);
 
	useEffect(() => {
		console.log("UseEffect with no dependency");
	});
 
	useLayoutEffect(() => {
		console.log("UseLayoutEffect with no dependency");
	});
 
	useEffect(() => {
		console.log("UseEffect with count: " + count);
	}, [count]);
 
	useLayoutEffect(() => {
		console.log("UseLayoutEffect with count: " + count);
	}, [count]);
 
	console.log("React component rendered");
 
	return (
		<div>
			<button
				onClick={() => {
					console.log("Increase Button Clicked");
					setCount((c) => c + 1);
				}}
			>
				Increase
			</button>
			<div>count: {count}</div>
		</div>
	);
};
 

Now, pause for a moment. What would you expect to see in the console when this component renders and you click the button?

Console Logs:

What’s Actually Happening?

Before you check, try to visualize it in your head. Done? Okay, here’s the breakdown:

  1. Initial Render:
    • The component renders, logging "React component rendered".
    • Both useEffect and useLayoutEffect hooks with empty dependency arrays fire once, logging their respective messages.
  2. No Dependencies:
    • useEffect and useLayoutEffect with no dependency array fire every time the component renders (initial and updates).
  3. With [count] Dependency:
    • The useEffect and useLayoutEffect hooks with [count] fire only when count changes.
  4. Button Click:
    • Clicking the button triggers setCount, causing a state update. This:
      • Re-renders the component, logging "Increase Button Clicked" and "React component rendered".
      • Triggers the hooks as described above.

tl;dr

  • useEffect with an empty array runs once after the component mounts.
  • useEffect without a dependency array runs after every render.
  • useLayoutEffect behaves similarly but fires earlier in the update cycle.
  • Including dependencies (e.g., [count]) ensures the hook runs only when those values change.

If you’re as obsessed with debugging React behavior as I am, you’ll find this useful next time you’re in lifecycle chaos!

  • 02-20-2026

    The trap of making everything dynamic

  • 04-11-2025

    A try at type-safe groupBy function in TypeScript

  • 04-10-2025

    Email special headers

  • 04-10-2025

    Adding prettier to eslint

  • 04-09-2025

    Storing Vector in Postgres with Drizzle ORM