<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Sam Abaasi on Medium]]></title>
        <description><![CDATA[Stories by Sam Abaasi on Medium]]></description>
        <link>https://medium.com/@samabaasi?source=rss-2b516377a244------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*cd5UKK5v71ikBHh5rjZ27g.jpeg</url>
            <title>Stories by Sam Abaasi on Medium</title>
            <link>https://medium.com/@samabaasi?source=rss-2b516377a244------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 25 Jun 2026 14:18:09 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@samabaasi/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[From React to NestJS Master (Part 3) : Event Sourcing: Git for Your Database — And the Complete…]]></title>
            <link>https://javascript.plainenglish.io/from-react-to-nestjs-master-part-3-event-sourcing-git-for-your-database-and-the-complete-2152f92fd4f6?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/2152f92fd4f6</guid>
            <category><![CDATA[nestjs]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Fri, 15 May 2026 10:36:50 GMT</pubDate>
            <atom:updated>2026-05-15T11:05:15.362Z</atom:updated>
            <content:encoded><![CDATA[<h3>From React to NestJS Master (Part 3) : Event Sourcing: Git for Your Database — And the Complete Picture</h3><h3>From React to NestJS Master · Part 3 of 3</h3><p><em>In </em><a href="https://samabaasi.medium.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-3be4664c2316"><strong><em>Part 1</em></strong></a><em> we built Hexagonal Architecture — swappable databases behind clean ports. In </em><a href="https://samabaasi.medium.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-f2646c40f46d"><strong><em>Part 2</em></strong><em> </em></a><em>we added CQRS and Events — Redux for the backend. Now in </em><strong><em>Part 3</em></strong><em> we add Event Sourcing: Git version control plus Redux DevTools time travel, but for your entire database.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ySosxPLvaDaXFCoFGaHmTA.png" /><figcaption>From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture (Part 3) Event Sourcing: Git for Your Database — And the Complete Picture</figcaption></figure><h3>What you’ll walk away with</h3><p>By the end of this article you’ll understand <strong>Event Sourcing</strong> (storing every state change as an immutable event), the <strong>Event Store</strong> (the repository that holds all your events), <strong>Rehydration</strong> (rebuilding current state by replaying events), <strong>Snapshots</strong> (the performance escape hatch), and finally how all three parts of this series snap together into one coherent architecture.</p><h3>The Problem Event Sourcing Solves</h3><p>Imagine you’re building a collaborative document editor — think Google Docs. You have two choices for how to store state.</p><p><strong>Option 1: Store only the current document.</strong></p><pre>const document = {<br>  id: &#39;doc-123&#39;,<br>  content: &#39;Hello World&#39;,<br>  lastModified: &#39;2024-01-15T10:00:00Z&#39;<br>};<br>// ❌ You can&#39;t see what changed<br>// ❌ You can&#39;t undo to a specific point<br>// ❌ You can&#39;t debug &quot;how did this typo appear?&quot;<br>// ❌ Two people editing at once = lost changes</pre><p><strong>Option 2: Store every change.</strong></p><pre>const events = [<br>  { type: &#39;INSERT&#39;, position: 0, text: &#39;H&#39;, timestamp: &#39;10:00:01&#39; },<br>  { type: &#39;INSERT&#39;, position: 1, text: &#39;e&#39;, timestamp: &#39;10:00:02&#39; },<br>  { type: &#39;DELETE&#39;, position: 5, text: &#39; &#39;, timestamp: &#39;10:00:06&#39; },<br>  { type: &#39;INSERT&#39;, position: 5, text: &#39;!&#39;, timestamp: &#39;10:00:07&#39; },<br>];<br>// ✅ Perfect audit trail<br>// ✅ Time travel to any point<br>// ✅ Real-time collaboration<br>// ✅ Undo/redo forever</pre><p>Event Sourcing is exactly this pattern for your backend. And if you already know Git, you already know Event Sourcing — you just don’t know you know it yet.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H56xMdgmIB897URnfeUTDQ.gif" /><figcaption>Git ↔ Event Sourcing analogy table</figcaption></figure><p>Git concept Event Sourcing concept Why they’re the same Commit Event Immutable record of a change .git folder Event Store Storage for all events git log Event Stream Full ordered history git checkout Rehydration Rebuild state from history Git tag Snapshot Save point for performance git bisect Time travel debugging Find when the bug appeared Merge conflict Concurrency error Two changes to the same state</p><blockquote><strong><em>If you understand Git, you already understand Event Sourcing. The only difference: Git tracks code. Event Sourcing tracks data.</em></strong></blockquote><h3>Traditional Database vs. Event Sourcing</h3><p>The difference becomes visceral when you ask real business questions.</p><p><strong>With a traditional database</strong> you only ever see the final state:</p><pre>SELECT * FROM alarms WHERE id = &#39;alm-123&#39;;<br>-- { id: &#39;alm-123&#39;, name: &#39;Fire&#39;, severity: &#39;critical&#39;, acknowledged: true }<br>-- Questions you CANNOT answer:<br>-- Was the severity ever changed?<br>-- Who acknowledged it and when?<br>-- What was the original severity?<br>-- How long between creation and acknowledgment?</pre><p><strong>With Event Sourcing</strong> you store every transition:</p><pre>const alarmEvents = [<br>  {<br>    id: &#39;evt-1&#39;, streamId: &#39;alm-123&#39;, type: &#39;AlarmCreated&#39;, position: 0,<br>    data: { name: &#39;Fire&#39;, severity: &#39;high&#39; },<br>    timestamp: &#39;2024-01-15T10:00:00Z&#39;, userId: &#39;system&#39;<br>  },<br>  {<br>    id: &#39;evt-2&#39;, streamId: &#39;alm-123&#39;, type: &#39;AlarmSeverityUpdated&#39;, position: 1,<br>    data: { oldSeverity: &#39;high&#39;, newSeverity: &#39;critical&#39; },<br>    timestamp: &#39;2024-01-15T10:05:00Z&#39;, userId: &#39;operator-456&#39;<br>  },<br>  {<br>    id: &#39;evt-3&#39;, streamId: &#39;alm-123&#39;, type: &#39;AlarmAcknowledged&#39;, position: 2,<br>    data: { acknowledgedBy: &#39;supervisor-789&#39;, notes: &#39;Fire confirmed&#39; },<br>    timestamp: &#39;2024-01-15T10:10:00Z&#39;, userId: &#39;supervisor-789&#39;<br>  }<br>];<br>// NOW every question has an answer:<br>// Original severity?   → &#39;high&#39; (event 0)<br>// Who acknowledged?    → supervisor-789 at 10:10<br>// Response time?       → 10 minutes (event 2 − event 0)<br>// Who escalated it?    → operator-456 at 10:05</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vO7YPoRnZqHnffLrWqW2CQ.gif" /><figcaption>Alarm lifecycle event stream — shows the same 3 events as a live timeline</figcaption></figure><p>The events <em>are</em> the source of truth. The “current state” you see in a traditional database is just a projection — a convenience view derived from the full history.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YIUyfccc3MwC9TGRV7wl6A.gif" /><figcaption>Traditional DB vs Event Sourcing</figcaption></figure><h3>Building Event Sourcing Step by Step</h3><h3>Step 1: The Event Store schema</h3><p>Think of this as your .git folder — append-only, ordered, indexed by stream.</p><pre>// src/shared/infrastructure/event-store/event.schema.ts<br>@Schema({ timestamps: true })<br>export class EventDocument extends Document {<br>  @Prop({ required: true, index: true })<br>  streamId: string;      // Which aggregate — e.g. &#39;alarm:alm-123&#39;<br>  @Prop({ required: true })<br>  type: string;          // Event type — e.g. &#39;AlarmCreated&#39;<br>  @Prop({ required: true })<br>  position: number;      // Order in stream: 0, 1, 2…<br>  @Prop({ required: true, type: Object })<br>  data: Record&lt;string, any&gt;;<br>  @Prop({ type: Object })<br>  metadata?: Record&lt;string, any&gt;;  // Who, when, why, correlation ID<br>}<br>// Unique index prevents duplicate positions in a stream —<br>// just like Git prevents two commits at the same hash.<br>EventSchema.index({ streamId: 1, position: 1 }, { unique: true });</pre><h3>Step 2: The Aggregate Root</h3><p>The aggregate is the Git repo. It can apply events to change its state, track uncommitted changes, and rebuild itself from a history of past events.</p><pre>// src/alarms/domain/alarm.aggregate.ts<br>export class AlarmAggregate extends AggregateRoot {<br>  private _id: string;<br>  private _name: string;<br>  private _severity: string;<br>  private _isAcknowledged: boolean = false;<br>  private _version: number = 0;<br>  private _changes: any[] = [];  // Staging area — like git add<br>  // Apply an event to update state (like git apply)<br>  private applyEvent(event: any, isFromHistory = false) {<br>    const handler = this[`on${event.constructor.name}`];<br>    if (!handler) throw new Error(`No handler for ${event.constructor.name}`);<br>    handler.call(this, event);<br>    if (!isFromHistory) this._changes.push(event);<br>  }<br>  // Business method — creates and applies an event (like git commit)<br>  createAlarm(name: string, severity: string, facilityId: string) {<br>    if (!name || name.length &lt; 3)<br>      throw new Error(&#39;Alarm name must be at least 3 characters&#39;);<br>    this.applyEvent(new AlarmCreatedEvent(this.generateId(), name, severity, facilityId));<br>  }<br>  onAlarmCreatedEvent(event: AlarmCreatedEvent) {<br>    this._id = event.alarmId;<br>    this._name = event.name;<br>    this._severity = event.severity;<br>    this._version = 1;<br>  }<br>  acknowledgeAlarm(acknowledgedBy: string) {<br>    if (this._isAcknowledged) throw new Error(&#39;Alarm already acknowledged&#39;);<br>    this.applyEvent(new AlarmAcknowledgedEvent(this._id, acknowledgedBy));<br>  }<br>  onAlarmAcknowledgedEvent(event: AlarmAcknowledgedEvent) {<br>    this._isAcknowledged = true;<br>    this._acknowledgedAt = event.timestamp;<br>  }<br>  getUncommittedEvents() { return this._changes; }   // git status<br>  commit() { this._changes = []; }                   // git reset (after push)<br>  // Rebuild from history — this is git checkout<br>  loadFromHistory(events: any[]) {<br>    for (const event of events) {<br>      this.applyEvent(event, true);<br>      this._version++;<br>    }<br>  }<br>}</pre><h3>Step 3: The Event Store service</h3><pre>// src/shared/infrastructure/event-store/mongo-event-store.ts<br>@Injectable()<br>export class MongoEventStore {<br>  async appendEvents(streamId: string, events: any[], expectedVersion: number) {<br>    const eventsWithPositions = events.map((event, index) =&gt; ({<br>      streamId,<br>      type: event.constructor.name,<br>      position: expectedVersion + index + 1,<br>      data: this.serializeEvent(event),<br>      metadata: { timestamp: new Date(), correlationId: this.generateCorrelationId() }<br>    }));<br>    try {<br>      await this.eventModel.insertMany(eventsWithPositions);<br>    } catch (error) {<br>      if (error.code === 11000) {  // Duplicate key — like a merge conflict<br>        throw new ConcurrencyError(<br>          `Stream ${streamId} was modified. Expected version ${expectedVersion}`<br>        );<br>      }<br>      throw error;<br>    }<br>  }<br>  async getEvents(streamId: string) {<br>    const docs = await this.eventModel.find({ streamId }).sort({ position: 1 }).exec();<br>    return docs.map(doc =&gt; this.deserializeEvent(doc));<br>  }<br>  async getEventsFromPosition(streamId: string, fromPosition: number) {<br>    const docs = await this.eventModel<br>      .find({ streamId, position: { $gt: fromPosition } })<br>      .sort({ position: 1 }).exec();<br>    return docs.map(doc =&gt; this.deserializeEvent(doc));<br>  }<br>}</pre><h3>Step 4: Rehydration — git checkout for your data</h3><pre>// src/alarms/application/services/alarm-rehydrator.service.ts<br>@Injectable()<br>export class AlarmRehydrator {<br>  // Rebuild the current state — like git checkout main<br>  async rehydrate(alarmId: string): Promise&lt;AlarmAggregate&gt; {<br>    const events = await this.eventStore.getEvents(`alarm:${alarmId}`);<br>    if (events.length === 0) throw new Error(`Alarm ${alarmId} not found`);<br>    const alarm = new AlarmAggregate();<br>    alarm.loadFromHistory(events);<br>    return this.eventPublisher.mergeObjectContext(alarm);<br>  }<br>  // Rebuild state at a specific moment — like git checkout &lt;commit-hash&gt;<br>  async rehydrateAtTime(alarmId: string, timestamp: Date): Promise&lt;AlarmAggregate&gt; {<br>    const allEvents = await this.eventStore.getEvents(`alarm:${alarmId}`);<br>    const eventsUpToTime = allEvents.filter(e =&gt; e._metadata.timestamp &lt;= timestamp);<br>    const alarm = new AlarmAggregate();<br>    alarm.loadFromHistory(eventsUpToTime);<br>    return this.eventPublisher.mergeObjectContext(alarm);<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MaKL8gA4NzcjRHwqXDjn1g.gif" /><figcaption>Rehydration &amp; time travel scrubber</figcaption></figure><h3>Step 5: Command handlers that use the event store</h3><p>Every command handler follows the same pattern: rehydrate the aggregate, apply business logic, append the resulting events, clear the staging area.</p><pre>@CommandHandler(AcknowledgeAlarmCommand)<br>export class AcknowledgeAlarmHandler implements ICommandHandler&lt;AcknowledgeAlarmCommand&gt; {<br>  async execute(command: AcknowledgeAlarmCommand) {<br>    // 1. Rebuild aggregate from history (git checkout latest commit)<br>    const alarm = await this.rehydrator.rehydrate(command.alarmId);<br>    // 2. Apply business logic — creates new events internally<br>    alarm.acknowledgeAlarm(command.acknowledgedBy);<br>    // 3. Append only the new events (git commit + push)<br>    await this.eventStore.appendEvents(<br>      `alarm:${command.alarmId}`,<br>      alarm.getUncommittedEvents(),<br>      alarm.version - alarm.getUncommittedEvents().length<br>    );<br>    // 4. Clear staging area<br>    alarm.commit();<br>  }<br>}</pre><h3>Snapshots — The Performance Escape Hatch</h3><p><strong>The problem:</strong> an aggregate that has processed 10,000 events must replay all 10,000 every time it’s rehydrated. Over time this becomes a real performance issue.</p><p><strong>The solution:</strong> periodically save a snapshot of the current state — exactly like a Git tag — and only replay events <em>after</em> that snapshot.</p><pre>@Injectable()<br>export class AlarmRehydratorWithSnapshots {<br>  async rehydrate(alarmId: string): Promise&lt;AlarmAggregate&gt; {<br>    const streamId = `alarm:${alarmId}`;<br>    // Find the latest snapshot (like finding the latest tag)<br>    const latestSnapshot = await this.snapshotModel<br>      .findOne({ streamId }).sort({ version: -1 }).exec();<br>    let alarm: AlarmAggregate;<br>    let eventsSinceSnapshot: any[];<br>    if (latestSnapshot) {<br>      // Restore from snapshot, then replay only the delta<br>      alarm = new AlarmAggregate();<br>      alarm.loadFromSnapshot(latestSnapshot.state);<br>      eventsSinceSnapshot = await this.eventStore.getEventsFromPosition(<br>        streamId, latestSnapshot.version<br>      );<br>    } else {<br>      alarm = new AlarmAggregate();<br>      eventsSinceSnapshot = await this.eventStore.getEvents(streamId);<br>    }<br>    alarm.loadFromHistory(eventsSinceSnapshot);<br>    // Create a new snapshot every 100 events<br>    if (alarm.version - (latestSnapshot?.version || 0) &gt;= 100) {<br>      await this.createSnapshot(alarm);<br>    }<br>    return alarm;<br>  }<br>}</pre><p>The performance difference is dramatic:</p><p>Total events Without snapshots With snapshots (every 100) Speedup 100 Replay 100 Replay 100 1× 1,000 Replay 1,000 Snapshot + replay 100 <strong>10×</strong> 10,000 Replay 10,000 Snapshot + replay 100 <strong>100×</strong> 100,000 Replay 100,000 Snapshot + replay 100 <strong>1,000×</strong></p><blockquote><strong><em>Snapshots keep rehydration O(1) + O(100) regardless of how much history has accumulated.</em></strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZW8d5pd5Bvgd_9meLyIzTw.gif" /><figcaption>Snapshot performance optimization</figcaption></figure><h3>Time Travel in Practice</h3><p>Let’s trace what happens when an alarm is created, escalated, and acknowledged — and how you can rewind to any moment.</p><pre>// 1. Create alarm at 10:00<br>await commandBus.execute(new CreateAlarmCommand(&#39;Fire&#39;, &#39;high&#39;, &#39;plant-1&#39;));<br>// Event store: [AlarmCreated { severity: &#39;high&#39; }]<br>// 2. Operator escalates at 10:05<br>await commandBus.execute(new UpdateAlarmSeverityCommand(&#39;alm-123&#39;, &#39;critical&#39;));<br>// Event store: [..., AlarmSeverityUpdated { high → critical }]<br>// 3. Supervisor acknowledges at 10:10<br>await commandBus.execute(new AcknowledgeAlarmCommand(&#39;alm-123&#39;, &#39;supervisor-456&#39;));<br>// Event store: [..., AlarmAcknowledged { by: supervisor-456 }]<br>// ── Current state ──────────────────────────────────────────<br>const current = await rehydrator.rehydrate(&#39;alm-123&#39;);<br>// { severity: &#39;critical&#39;, acknowledged: true }<br>// ── Time travel to just after creation ─────────────────────<br>const atCreation = await rehydrator.rehydrateAtTime(&#39;alm-123&#39;, new Date(&#39;10:00&#39;));<br>// { severity: &#39;high&#39;, acknowledged: false }<br>// ── Answer business questions from the event log ───────────<br>const events = await eventStore.getEvents(&#39;alarm:alm-123&#39;);<br>const responseTime = events[2].timestamp - events[0].timestamp; // 10 minutes<br>const escalatedBy  = events[1].metadata.userId;                 // operator-789<br>const origSeverity = events[0].data.severity;                   // &#39;high&#39;</pre><p>None of this is possible with a traditional database. Every question gets answered directly from the immutable event log.</p><h3>The Complete Architecture</h3><p>After three parts, the full picture looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SKdsruBCs36Hrp2F6dRbYw.gif" /><figcaption>Complete architecture diagram — the animated version is far clearer</figcaption></figure><p><strong>Presentation</strong> routes requests. <strong>CQRS</strong> separates reads from writes so each can be optimised independently. <strong>Event Sourcing</strong> makes the event log the single source of truth. <strong>Infrastructure</strong> sits behind ports — swappable without touching business logic.</p><p>Each layer has exactly one job. That’s the whole game.</p><h3>A Real-World Flow</h3><p>An IoT temperature sensor fires. Here’s everything that happens end to end.</p><pre>// 1. Frontend sends: POST /api/alarms<br>// { name: &#39;Temperature Critical&#39;, severity: &#39;high&#39;, facilityId: &#39;plant-123&#39; }<br>// 2. Controller dispatches a command<br>@Post()<br>async create(@Body() dto: CreateAlarmDto) {<br>  return this.commandBus.execute(new CreateAlarmCommand(dto));<br>}<br>// 3. Command handler creates and stores events<br>@CommandHandler(CreateAlarmCommand)<br>class CreateAlarmHandler {<br>  async execute(command) {<br>    const alarm = new AlarmAggregate();<br>    alarm.createAlarm(command.name, command.severity, command.facilityId);<br>    const events = alarm.getUncommittedEvents();<br>    await this.eventStore.appendEvents(`alarm:${alarm.id}`, events, 0);<br>    events.forEach(e =&gt; this.eventBus.publish(e));<br>    return { id: alarm.id };<br>  }<br>}<br>// 4. Event handler keeps the Read DB in sync<br>@EventsHandler(AlarmCreatedEvent)<br>class UpdateReadDbHandler {<br>  async handle(event: AlarmCreatedEvent) {<br>    await this.readRepository.upsert({<br>      id: event.alarmId, name: event.name,<br>      severity: event.severity, status: &#39;pending&#39;,<br>      severityColor: this.getColor(event.severity)<br>    });<br>  }<br>}<br>// 5. Saga detects a dangerous pattern — 3 alarms in 5 seconds<br>@Saga()<br>class PatternDetectionSaga {<br>  detectThreeInFiveSeconds(events$) {<br>    return events$.pipe(<br>      ofType(AlarmCreatedEvent),<br>      groupBy(e =&gt; e.facilityId),<br>      mergeMap(group =&gt; group.pipe(<br>        bufferTime(5000),<br>        filter(events =&gt; events.length &gt;= 3),<br>        map(events =&gt; new NotifySupervisorCommand(events[0].facilityId, events.length))<br>      ))<br>    );<br>  }<br>}<br>// 6. Meanwhile the dashboard query is fast — reads from the denormalized Read DB<br>@Get(&#39;dashboard/:facilityId&#39;)<br>async getDashboard(@Param(&#39;facilityId&#39;) id: string) {<br>  return this.queryBus.execute(new GetDashboardQuery(id));<br>}</pre><p>The write path is event-sourced and auditable. The read path is a denormalized, pre-shaped projection optimised for the UI. They evolve independently.</p><h3>Testing Event Sourcing</h3><p>Because aggregates are pure functions of their event history, testing is straightforward.</p><pre>describe(&#39;AlarmRehydrator&#39;, () =&gt; {<br>  it(&#39;should rebuild aggregate from events&#39;, async () =&gt; {<br>    const events = [<br>      new AlarmCreatedEvent(&#39;alm-1&#39;, &#39;Fire&#39;, &#39;high&#39;, &#39;plant-1&#39;),<br>      new AlarmAcknowledgedEvent(&#39;alm-1&#39;, &#39;supervisor-1&#39;)<br>    ];<br>    eventStore.getEvents.mockResolvedValue(events);<br>    const alarm = await rehydrator.rehydrate(&#39;alm-1&#39;);<br>    expect(alarm.name).toBe(&#39;Fire&#39;);<br>    expect(alarm.severity).toBe(&#39;high&#39;);<br>    expect(alarm.isAcknowledged).toBe(true);<br>  });<br>  it(&#39;should reject concurrent modifications&#39;, async () =&gt; {<br>    // Two handlers rehydrate the same aggregate simultaneously<br>    const alarm1 = await rehydrator.rehydrate(&#39;alm-1&#39;);<br>    const alarm2 = await rehydrator.rehydrate(&#39;alm-1&#39;);<br>    alarm1.acknowledgeAlarm(&#39;supervisor-1&#39;);<br>    alarm2.acknowledgeAlarm(&#39;supervisor-2&#39;);<br>    await expect(save(alarm1)).resolves.not.toThrow();<br>    await expect(save(alarm2)).rejects.toThrow(ConcurrencyError);<br>  });<br>});</pre><p>No database spin-up needed. Give the aggregate a sequence of events, assert on the resulting state. That’s it.</p><h3>When to Use (and When to Skip) Event Sourcing</h3><p>Event Sourcing carries real costs. Be honest about whether the benefits justify them for your situation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rRh8c1O0aAZMyFQbp5cCIA.gif" /><figcaption>When to use Event Sourcing decision card</figcaption></figure><p><strong>Use it when:</strong></p><ul><li><strong>Audit requirements are non-negotiable.</strong> Financial systems, healthcare records, compliance logs — if you must answer “who changed what and when” with certainty, events are the only honest answer.</li><li><strong>You need time travel debugging.</strong> When a production bug is “impossible to reproduce,” being able to replay the exact sequence of events that caused it is worth its weight in gold.</li><li><strong>Undo/redo is a product feature.</strong> Compensating events cleanly reverse any past operation without destructive updates.</li><li><strong>The system is collaborative.</strong> Real-time concurrent editing (Google Docs, Figma-style) is far easier to reason about when the source of truth is an ordered event log.</li></ul><p><strong>Skip it when:</strong></p><ul><li><strong>It’s a CRUD app.</strong> Todo lists, admin panels, basic internal tools. The complexity cost is enormous and the benefits are zero.</li><li><strong>Storage is constrained.</strong> Events take 10–100× more space than storing only current state.</li><li><strong>Your team is small and the deadline is real.</strong> Event store, snapshots, rehydration, concurrency handling — it’s a steep learning curve for a junior team under time pressure.</li><li><strong>History simply doesn’t matter.</strong> If no one will ever ask “what did this look like six months ago?”, you’re paying all the cost for none of the value.</li></ul><blockquote><strong><em>The decision tree:</em></strong><em> start with Hexagonal Architecture (always worth it). Add CQRS when your reads are slowing your writes. Add Event Sourcing when your business needs a permanent, queryable history.</em></blockquote><h3>The Complete Frontend → Backend Mapping</h3><p>After three parts, here’s the full picture of how your existing frontend intuition maps to backend patterns.</p><p>Frontend concept Backend concept Part Why they’re analogous React component Controller 1 Entry point, delegates work Custom hook Service 1 Orchestrates operations Context provider Module 1 Provides dependencies useState Aggregate state 1 Holds current state Redux action Command 2 Describes what should change Redux reducer Command handler 2 Applies state changes Redux selector Query 2 Reads state efficiently useEffect (basic) Event handler 2 Reacts to state changes useEffect (complex) Saga 2 Complex reactive logic Redux DevTools Event Store 3 Records all state changes Time travel debugging Rehydration 3 Replays past states Git commit Event 3 Immutable change record git checkout Rehydration 3 Restores state from history Git tag Snapshot 3 Performance save point Merge conflict Concurrency error 3 Two changes to the same state</p><h3>The Challenge: Build a Banking System</h3><p>The best way to solidify this is to build something yourself. Here’s a starter — finish it.</p><pre>class BankAccount extends AggregateRoot {<br>  private balance: number = 0;<br>  createAccount(initialBalance: number) {<br>    // Emit AccountCreatedEvent<br>  }<br>  deposit(amount: number, reference: string) {<br>    // Emit MoneyDepositedEvent<br>  }<br>  withdraw(amount: number, reference: string) {<br>    if (amount &gt; this.balance) throw new Error(&#39;Insufficient funds&#39;);<br>    // Emit MoneyWithdrawnEvent<br>  }<br>  onAccountCreatedEvent(e: AccountCreatedEvent)  { this.balance = e.initialBalance; }<br>  onMoneyDepositedEvent(e: MoneyDepositedEvent)  { this.balance += e.amount; }<br>  onMoneyWithdrawnEvent(e: MoneyWithdrawnEvent)  { this.balance -= e.amount; }<br>}</pre><p>Your checklist: can you create an account? Deposit and withdraw? Does it prevent overdrawing? Can you view the full transaction history? Rewind to any past balance? Does a Saga detect 3 withdrawals in under 5 minutes and raise a fraud alert?</p><p>If you can do all of that, you’ve genuinely understood this series.</p><h3>The Aha Moment</h3><pre>If you understand:           Then you already understand:<br>─────────────────────        ─────────────────────────────<br>Redux actions/reducers    →  CQRS commands/handlers<br>Redux DevTools            →  Event Store<br>Git commits               →  Events<br>git checkout              →  Rehydration<br>Git tags                  →  Snapshots<br>React useEffect           →  Sagas</pre><pre>The only difference: in the frontend, state lives in memory.<br>In the backend, state lives in databases and event stores.</pre><p>That’s the whole secret. The mental models you’ve built over years of frontend work translate directly. You were always closer to backend architecture than you thought.</p><h3>Series Recap</h3><p><a href="https://samabaasi.medium.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-3be4664c2316"><strong>Part 1 — Hexagonal Architecture</strong></a><strong>:</strong> separate business logic from infrastructure. The application defines ports; infrastructure implements adapters. Always worth the small upfront cost.</p><p><a href="https://samabaasi.medium.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-f2646c40f46d"><strong>Part 2 — CQRS + Events</strong></a><strong>:</strong> separate reads from writes so each can evolve and scale independently. Sagas handle the complex reactive logic that would otherwise tangle your command handlers.</p><p><strong>Part 3 — Event Sourcing:</strong> make the event log the source of truth. Current state is just a projection. Snapshots keep performance linear. Rehydration gives you time travel for free.</p><p>Start simple. Add each layer only when you feel the problem it solves. Hexagonal architecture is nearly free. CQRS pays off when reads start hurting writes. Event Sourcing pays off when history and auditability are first-class requirements.</p><p><em>If this series helped you understand backend architecture as a frontend engineer — share it with someone else making the jump.</em></p><p><em>Follow for: real-world case studies, performance optimization deep dives, microservices with these patterns, and testing strategies for event-sourced systems.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2152f92fd4f6" width="1" height="1" alt=""><hr><p><a href="https://javascript.plainenglish.io/from-react-to-nestjs-master-part-3-event-sourcing-git-for-your-database-and-the-complete-2152f92fd4f6">From React to NestJS Master (Part 3) : Event Sourcing: Git for Your Database — And the Complete…</a> was originally published in <a href="https://javascript.plainenglish.io">JavaScript in Plain English</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture (Part 2)]]></title>
            <link>https://blog.stackademic.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-f2646c40f46d?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/f2646c40f46d</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[nestjs]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Wed, 13 May 2026 20:49:18 GMT</pubDate>
            <atom:updated>2026-05-25T08:56:12.903Z</atom:updated>
            <content:encoded><![CDATA[<h3>Part 2: Commands, Queries, Events, and Sagas — The Backend’s Redux + useEffect</h3><blockquote><a href="https://medium.com/javascript-in-plain-english/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-3be4664c2316"><strong><em>In Part 1</em></strong></a><em> we built a Hexagonal architecture that lets us swap databases like changing state managers. Now we’ll add the patterns that make complex systems manageable: CQRS (like Redux actions + selectors) and Event-Driven Architecture (like useEffect on steroids).</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ySosxPLvaDaXFCoFGaHmTA.png" /><figcaption>From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture</figcaption></figure><h3>The Problem with CRUD</h3><p>You’ve probably built a Twitter-like feed before. One endpoint that does everything — reads, writes, validation, caching. It works until it doesn’t.</p><p>The problem is that <strong>reads and writes have completely different requirements</strong>:</p><p>Write Operations Read Operations Need validation rules Need FAST performance Need data integrity Can be denormalized Happen less frequently Happen CONSTANTLY Complex business logic Simple data shaping One item at a time Hundreds at once</p><p>In React terms: this is like using the same component for both editing AND displaying a form, with all validation and submission logic mixed with rendering.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UzmiRHCutZ5yncW2qOG-bQ.gif" /></figure><h3>CQRS — The Core Insight</h3><p><strong>CQRS = Command Query Responsibility Segregation.</strong></p><pre>COMMAND (Write)              QUERY (Read)<br>──────────────────           ──────────────────<br>&quot;Change something&quot;           &quot;Get something&quot;<br>Returns void (or ID)         Returns data<br>Has side effects             No side effects<br>Validates business rules     Optimized for speed<br>Goes through event bus       Direct database access</pre><p>The React analogy that makes it click:</p><pre>// Redux — you already know this pattern<br>const action   = { type: &#39;ALARM_CREATED&#39;, payload: { text: &#39;...&#39; } }; // Command<br>const reducer  = (state, action) =&gt; { /* applies business rules */ };  // Command Handler<br>const selector = (state) =&gt; state.alarms.filter(a =&gt; !a.hidden);      // Query<br>function AlarmApp() {<br>  const dispatch = useDispatch(); // For commands<br>  const alarms   = useSelector(selector); // For queries<br>}</pre><p>CQRS is exactly this pattern — but for your entire backend:</p><p>Redux Concept CQRS Concept Action Command Reducer Command Handler Selector Query Store (write) Write Database Store (read) Read Database dispatch() CommandBus useSelector() QueryBus</p><h3>Building CQRS Step by Step</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LVvwgPgYbqTvBmjU2to3Lg.gif" /><figcaption>Building CQRS Step by Step</figcaption></figure><h3>Install the package</h3><pre>npm install @nestjs/cqrs</pre><h3>Define a Command (like a Redux action)</h3><pre>// src/alarms/application/commands/create-alarm.command.ts<br>export class CreateAlarmCommand {<br>  constructor(<br>    public readonly name: string,<br>    public readonly severity: string,<br>    public readonly facilityId: string,<br>  ) {}<br>}</pre><h3>Create the Command Handler (like a Redux reducer)</h3><pre>// src/alarms/application/commands/create-alarm.command-handler.ts<br>@CommandHandler(CreateAlarmCommand)<br>export class CreateAlarmCommandHandler implements ICommandHandler&lt;CreateAlarmCommand&gt; {<br>  constructor(<br>    private readonly alarmRepository: AlarmRepository,<br>    private readonly eventBus: EventBus<br>  ) {}<br>  async execute(command: CreateAlarmCommand): Promise&lt;Alarm&gt; {<br>    // 1. Validate (domain logic)<br>    const severity = Severity.create(command.severity);<br>    // 2. Create entity (domain logic)<br>    const alarm = new Alarm(this.generateId(), command.name, severity, command.facilityId);<br>    // 3. Persist (write DB via port)<br>    await this.alarmRepository.save(alarm);<br>    // 4. Broadcast what happened<br>    this.eventBus.publish(new AlarmCreatedEvent(alarm));<br>    return alarm;<br>  }<br>}</pre><p>Notice what’s NOT here: no @Get(), no HTTP status codes, no database queries. This handler works whether you call it from HTTP, GraphQL, WebSockets, or a CLI.</p><h3>Define a Query (like a Redux selector)</h3><pre>// src/alarms/application/queries/get-dashboard-alarms.query.ts<br>export class GetDashboardAlarmsQuery {<br>  constructor(<br>    public readonly facilityId: string,<br>    public readonly limit: number = 50,<br>    public readonly onlyUrgent: boolean = false<br>  ) {}<br>}</pre><h3>Create the Query Handler</h3><pre>@QueryHandler(GetDashboardAlarmsQuery)<br>export class GetDashboardAlarmsQueryHandler implements IQueryHandler&lt;GetDashboardAlarmsQuery&gt; {<br>  constructor(<br>    // A DIFFERENT repository — optimized for reads<br>    private readonly readRepository: AlarmReadRepository<br>  ) {}<br>  async execute(query: GetDashboardAlarmsQuery): Promise&lt;DashboardAlarmDto[]&gt; {<br>    let alarms = await this.readRepository.findByFacility(query.facilityId);<br>    if (query.onlyUrgent) {<br>      alarms = alarms.filter(a =&gt; a.isUrgent);<br>    }<br>    return alarms.slice(0, query.limit).map(alarm =&gt; ({<br>      id: alarm.id,<br>      title: alarm.name,<br>      severityColor: alarm.severityColor,<br>      time: this.formatRelativeTime(alarm.createdAt),<br>      requiresAction: !alarm.isAcknowledged &amp;&amp; alarm.isUrgent,<br>    }));<br>  }<br>}</pre><p>No business logic. No validation. Just fast data fetching shaped for the UI.</p><h3>The Controller — now clean</h3><pre>@Controller(&#39;alarms&#39;)<br>export class AlarmsController {<br>  constructor(<br>    private readonly commandBus: CommandBus,<br>    private readonly queryBus: QueryBus<br>  ) {}<br>  @Post()<br>  async create(@Body() body: CreateAlarmDto) {<br>    const alarm = await this.commandBus.execute(<br>      new CreateAlarmCommand(body.name, body.severity, body.facilityId)<br>    );<br>    return { id: alarm.id, requiresAttention: alarm.requiresImmediateAttention() };<br>  }<br>  @Get(&#39;dashboard/:facilityId&#39;)<br>  async getDashboard(@Param(&#39;facilityId&#39;) facilityId: string) {<br>    return this.queryBus.execute(new GetDashboardAlarmsQuery(facilityId));<br>  }<br>}</pre><h3>Domain Events — The Backend’s Custom Event System</h3><h3>The problem events solve</h3><p>In React, when a child component changes, you bubble an event up to the parent. The child doesn’t know who’s listening — it just announces what happened.</p><p>Domain events are the exact same pattern:</p><ul><li>Your business logic (the “child”) emits events when something important happens</li><li>Other parts of the system (the “parent”) react to those events</li><li>The business logic doesn’t know or care what reacts</li></ul><h3>Event Fan-out: one event, multiple independent handlers</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mtk-P5qcpb7kfJMtPZcpWA.gif" /><figcaption><em>viz-12-event-fanout</em></figcaption></figure><p>Without events (tightly coupled):</p><pre>// ❌ BAD: AlarmService knows about notifications<br>class AlarmService {<br>  async createAlarm(input) {<br>    const alarm = new Alarm(...);<br>    await this.repository.save(alarm);<br>    // Alarm creation now knows about notification logic<br>    const recentAlarms = await this.repository.findRecent(input.facilityId, 5000);<br>    if (recentAlarms.length &gt;= 3) {<br>      await this.notificationService.notifySupervisor(input.facilityId);<br>    }<br>    return alarm;<br>  }<br>}</pre><p>With events (loosely coupled):</p><pre>// ✅ GOOD: AlarmService just announces what happened<br>class AlarmService {<br>  async createAlarm(input) {<br>    const alarm = new Alarm(...);<br>    await this.repository.save(alarm);<br>    // Published once. Never looks back.<br>    this.eventBus.publish(new AlarmCreatedEvent(alarm));<br>    return alarm;<br>  }<br>}<br>// Elsewhere — completely separate code, zero coupling to AlarmService<br>@EventsHandler(AlarmCreatedEvent)<br>class UpdateDashboardHandler {<br>  async handle(event: AlarmCreatedEvent) {<br>    await this.readRepository.upsert({ id: event.alarmId, ... });<br>  }<br>}</pre><p>Adding a new handler (SlackNotifier, AuditLogger, AnalyticsTracker) requires <strong>zero changes to AlarmService</strong>. Just register a new @EventsHandler.</p><h3>Eventual Consistency — the trade-off</h3><p>When writes and reads are separated, the read DB is updated asynchronously after the event fires. This means there’s a brief window where data is stale.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FNXZYRXGFdVW_7dTuvUGgg.gif" /><figcaption><em>viz-13-eventual-consistency</em></figcaption></figure><p>The write DB is updated at t=5ms. The event fires at t=6ms. The read DB catches up at t=7ms. The HTTP response goes out at t=8ms.</p><p><strong>You accept a few milliseconds of staleness in exchange for a read DB that’s always fast and never blocked by write transactions.</strong> For most UIs, this is completely acceptable.</p><h3>Sagas — Complex useEffect With Better Tools</h3><h3>The problem sagas solve</h3><p><strong>Business requirement:</strong> “If a facility has 3 alarms within 5 seconds, notify the supervisor. But only notify once per minute.”</p><p>This is the kind of useEffect you dread writing:</p><pre>// React equivalent — painful and fragile<br>useEffect(() =&gt; {<br>  const facilityGroups = new Map();<br>  const interval = setInterval(() =&gt; {<br>    for (const [facilityId, events] of facilityGroups) {<br>      const recentEvents = events.filter(e =&gt; Date.now() - e.timestamp &lt; 5000);<br>      if (recentEvents.length &gt;= 3) {<br>        const lastNotify = lastNotifyTime.get(facilityId);<br>        if (!lastNotify || Date.now() - lastNotify &gt; 60000) {<br>          notifySupervisor(facilityId);<br>          lastNotifyTime.set(facilityId, Date.now());<br>        }<br>      }<br>    }<br>  }, 1000);<br>  return () =&gt; clearInterval(interval);<br>}, []);</pre><p>Sagas make this declarative using RxJS — the same library React Query and Redux Observable use.</p><h3>The Saga Pipeline</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OGByN9yYYBbva3gttlMdig.gif" /><figcaption><em>viz-10-saga-pipeline</em></figcaption></figure><pre>@Injectable()<br>export class AlarmPatternSaga {<br>  @Saga()<br>  detectThreeAlarmsInFiveSeconds(events$: Observable&lt;any&gt;): Observable&lt;ICommand&gt; {<br>    return events$.pipe(<br>      ofType(AlarmCreatedEvent),           // 1. filter type<br>      groupBy(event =&gt; event.facilityId),  // 2. split by facility<br>      mergeMap(facilityEvents$ =&gt; facilityEvents$.pipe(<br>        bufferTime(5000),                  // 3. collect 5 seconds<br>        filter(events =&gt; events.length &gt;= 3), // 4. need 3+<br>        throttleTime(60000),               // 5. once per minute<br>        map(events =&gt; new NotifySupervisorCommand(<br>          events[0].facilityId,<br>          events.length<br>        ))<br>      ))<br>    );<br>  }<br>}</pre><h3>Seeing it happen live</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rvyC47gyWR2uuvhFcGZkyw.gif" /><figcaption><em>viz-08-three-alarms</em></figcaption></figure><p>The saga watches the event stream silently. The alarm service never knows it exists. The notification fires automatically when the pattern matches.</p><h3>Another saga: the timeout pattern</h3><p><strong>Business requirement:</strong> “If an alarm isn’t acknowledged within 15 minutes, escalate to the manager.”</p><pre>@Injectable()<br>export class EscalationSaga {<br>  @Saga()<br>  escalateUnacknowledgedAlarms(events$: Observable&lt;any&gt;): Observable&lt;ICommand&gt; {<br>    return events$.pipe(<br>      ofType(AlarmCreatedEvent),<br>      mergeMap(alarmCreated =&gt;<br>        race(<br>          // Stream 1: wait for acknowledgment<br>          events$.pipe(<br>            ofType(AlarmAcknowledgedEvent),<br>            filter(ack =&gt; ack.alarmId === alarmCreated.alarmId),<br>            first()<br>          ),<br>          // Stream 2: wait 15 minutes<br>          timer(900000).pipe(map(() =&gt; alarmCreated))<br>        ).pipe(<br>          filter(result =&gt; result instanceof AlarmCreatedEvent),<br>          map(() =&gt; new EscalateAlarmCommand(alarmCreated.alarmId))<br>        )<br>      )<br>    );<br>  }<br>}</pre><p>If the alarm is acknowledged before 15 minutes: Stream 1 wins, race() cancels Stream 2. If 15 minutes pass with no acknowledgment: Stream 2 wins, escalation fires. Your alarm creation code knows none of this exists.</p><h3>The Complete Flow</h3><p>Let’s trace a single POST /alarms request through the entire system:</p><pre>t = 0ms   POST /alarms<br>t = 1ms   Controller → CommandBus<br>t = 2ms   Command Handler validates<br>t = 5ms   Alarm saved to PostgreSQL (write DB)<br>t = 6ms   AlarmCreatedEvent published<br>t = 7ms   Event Handler updates MongoDB (read DB)<br>t = 8ms   HTTP 201 returned to frontend<br>t = 10ms  Frontend receives response<br>t = 15ms  Frontend queries GET /alarms/dashboard<br>t = 18ms  Fast response from MongoDB (no joins)<br>t = 5000ms Saga detects 3rd alarm from same facility<br>t = 5001ms NotifySupervisorCommand dispatched<br>t = 5002ms Email sent to supervisor</pre><p>Total: HTTP request served in 8ms. Complex pattern detected at 5 seconds. <strong>The alarm creation code never had to know about the notification.</strong></p><h3>Testing the CQRS + Event System</h3><p>The biggest benefit of CQRS and events is testability. Each layer tests in complete isolation.</p><h3>Testing a command handler</h3><pre>describe(&#39;CreateAlarmCommandHandler&#39;, () =&gt; {<br>  let handler: CreateAlarmCommandHandler;<br>  let mockRepository: jest.Mocked&lt;AlarmRepository&gt;;<br>  let mockEventBus: jest.Mocked&lt;EventBus&gt;;<br>  beforeEach(() =&gt; {<br>    mockRepository = { save: jest.fn() } as any;<br>    mockEventBus   = { publish: jest.fn() } as any;<br>    handler        = new CreateAlarmCommandHandler(mockRepository, mockEventBus);<br>  });<br>  it(&#39;should create and save an alarm&#39;, async () =&gt; {<br>    const command = new CreateAlarmCommand(&#39;Fire&#39;, &#39;high&#39;, &#39;plant-1&#39;);<br>    const result  = await handler.execute(command);<br>    expect(mockRepository.save).toHaveBeenCalled();<br>    expect(mockEventBus.publish).toHaveBeenCalledWith(expect.any(AlarmCreatedEvent));<br>    expect(result.name).toBe(&#39;Fire&#39;);<br>  });<br>  it(&#39;should reject invalid severity&#39;, async () =&gt; {<br>    const command = new CreateAlarmCommand(&#39;Fire&#39;, &#39;invalid&#39;, &#39;plant-1&#39;);<br>    await expect(handler.execute(command)).rejects.toThrow(&#39;Invalid severity&#39;);<br>    expect(mockRepository.save).not.toHaveBeenCalled();<br>  });<br>});</pre><p>No HTTP server. No database. No file system. Just pure logic.</p><h3>Testing a saga</h3><pre>describe(&#39;AlarmPatternSaga&#39;, () =&gt; {<br>  it(&#39;should notify after 3 alarms in 5 seconds&#39;, (done) =&gt; {<br>    const saga    = new AlarmPatternSaga();<br>    const events$ = of(<br>      new AlarmCreatedEvent(&#39;1&#39;, &#39;Fire&#39;,  &#39;high&#39;,   &#39;plant-1&#39;, new Date()),<br>      new AlarmCreatedEvent(&#39;2&#39;, &#39;Smoke&#39;, &#39;medium&#39;, &#39;plant-1&#39;, new Date()),<br>      new AlarmCreatedEvent(&#39;3&#39;, &#39;Heat&#39;,  &#39;high&#39;,   &#39;plant-1&#39;, new Date())<br>    );<br>    saga.detectThreeAlarmsInFiveSeconds(events$).subscribe(command =&gt; {<br>      expect(command).toBeInstanceOf(NotifySupervisorCommand);<br>      expect(command.facilityId).toBe(&#39;plant-1&#39;);<br>      done();<br>    });<br>  });<br>});</pre><h3>When Should You Use CQRS + Events?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YC7YYDHjIMcqoP_gKvpo3A.gif" /><figcaption><em>cqrs-decision</em></figcaption></figure><p><strong>Use CQRS when:</strong></p><ul><li>Your read and write performance requirements differ significantly</li><li>GET endpoints are slow because of complex joins on the write DB</li><li>You need multiple read shapes (dashboard, mobile, API)</li><li>You need an audit trail — events are immutable records</li></ul><p><strong>Skip it when:</strong></p><ul><li>It’s simple CRUD with no complex business logic</li><li>You need strong read-your-writes consistency</li><li>Small team or tight deadline — two models doubles maintenance surface</li><li>You haven’t felt the pain yet — add it when slow queries actually hurt</li></ul><p>The golden rule: <strong>start with a simple architecture. Add CQRS only when you feel the pain of mixing reads and writes.</strong></p><h3>Part 2 Summary</h3><p><strong>CQRS</strong> = Commands (writes) + Queries (reads) separated</p><ul><li>Commands = Redux actions (change state, validate, persist, emit events)</li><li>Queries = Redux selectors (fast, denormalized reads)</li></ul><p><strong>Domain Events</strong> = Announcements from business logic, no coupling</p><ul><li>@EventsHandler = addEventListener for your domain</li></ul><p><strong>Sagas</strong> = Complex event stream processing with RxJS</p><ul><li>ofType → groupBy → bufferTime → filter → throttleTime → map</li><li>Like useEffect with proper tools for timing and grouping</li></ul><p><strong>The complete flow:</strong></p><ul><li>HTTP → Controller → CommandBus → Handler → WriteDB → Event</li><li>Event → EventHandler → ReadDB (eventual consistency, ~2ms)</li><li>Event → Saga → Command → Side Effect (pattern detection, ~5000ms)</li></ul><p><strong>The “aha!” moment:</strong></p><blockquote><strong>CQRS is just Redux for your backend. Sagas are just useEffect with better timing tools.</strong></blockquote><h3>What’s Coming in Part 3</h3><p>Part 3 adds <strong>Event Sourcing</strong> — which is like Git version control for your database:</p><ul><li><strong>Event Store</strong> = the .git folder for your data</li><li><strong>Rehydration</strong> = git checkout — rebuild any past state</li><li><strong>Snapshots</strong> = Git tags — performance optimization for large streams</li><li><strong>Time travel debugging</strong> = git bisect for production bugs</li></ul><p>We’ll build a system where you can answer: <em>“What did this alarm look like at 3:47 PM last Tuesday?”</em> — and get the exact answer by replaying events.</p><h3>Series Navigation</h3><blockquote><a href="https://medium.com/javascript-in-plain-english/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-3be4664c2316"><strong><em>Part 1:</em></strong><em> Hexagonal Architecture — read it</em></a><em> </em><strong><em>Part 2: CQRS &amp; Event-Driven Architecture</em></strong><em> ← you are here </em><strong><em>Part 3:</em></strong><em> Event Sourcing </em>(coming soon)</blockquote><p><em>Did this help? Clap if you want Part 3!</em></p><p><em>Questions? Drop them in the comments. I read every single one.</em></p><h3>Resources</h3><ul><li><a href="https://docs.nestjs.com/recipes/cqrs">NestJS CQRS documentation</a></li><li><a href="https://rxjs.dev/guide/operators">RxJS operators reference</a></li></ul><h3>Quick Reference: CQRS + Events Cheatsheet</h3><p>Concept React Equivalent NestJS Syntax Command Redux Action class CreateAlarmCommand {} Command Handler Redux Reducer @CommandHandler(CreateAlarmCommand) Query Redux Selector class GetAlarmsQuery {} Query Handler Selector function @QueryHandler(GetAlarmsQuery) Event Custom DOM event class AlarmCreatedEvent {} Event Handler addEventListener @EventsHandler(AlarmCreatedEvent) Saga Complex useEffect @Saga() watchForPatterns(events$) Command Bus dispatch() commandBus.execute(command) Query Bus useSelector() queryBus.execute(query) Event Bus EventEmitter eventBus.publish(event)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f2646c40f46d" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-f2646c40f46d">From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture (Part 2)</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture (Part 1)]]></title>
            <link>https://blog.stackademic.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-3be4664c2316?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/3be4664c2316</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[hexagonal-architecture]]></category>
            <category><![CDATA[nestjs]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Wed, 13 May 2026 20:03:54 GMT</pubDate>
            <atom:updated>2026-05-25T08:56:07.439Z</atom:updated>
            <content:encoded><![CDATA[<h3>Part 1: The Foundation — Hexagonal Architecture</h3><blockquote><strong><em>This series assumes you know React and have basic NestJS knowledge. Every concept is explained through frontend analogies you already understand.</em></strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ySosxPLvaDaXFCoFGaHmTA.png" /><figcaption>From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture</figcaption></figure><h3>The Problem You Already Know</h3><p>You’ve built React components before. And at some point, you’ve probably built one that did way too much.</p><p>Fetching data. Holding state. Running business logic. Rendering JSX. All in one place.</p><p>You know that’s wrong. You’ve refactored it. You know how to fix it.</p><p>Here’s the thing: <strong>the default NestJS structure has the exact same problem.</strong> And the fix is the exact same pattern — just with different names.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vgNTMyheF_PGaTLnqXMrhQ.gif" /></figure><p><em>Watch how one component doing everything becomes four clean, separated concerns.</em></p><h3>What NestJS Gives You (and Why It’s Not Enough)</h3><p>When you run nest new, you get a controller that does everything:</p><pre>// ❌ BAD: Controller handling everything<br>@Controller(&#39;alarms&#39;)<br>export class AlarmsController {<br>  private alarms = []; // In-memory &quot;database&quot;<br>  @Post()<br>  create(@Body() createAlarmDto: CreateAlarmDto) {<br>    // Business logic mixed with HTTP handling<br>    const alarm = {<br>      id: Date.now().toString(),<br>      ...createAlarmDto,<br>      isAcknowledged: false,<br>      createdAt: new Date(),<br>    };<br>    this.alarms.push(alarm);<br>    return alarm;<br>  }<br>  @Get()<br>  findAll() {<br>    return this.alarms; // Direct data access<br>  }<br>  @Patch(&#39;:id/acknowledge&#39;)<br>  acknowledge(@Param(&#39;id&#39;) id: string) {<br>    const alarm = this.alarms.find(a =&gt; a.id === id);<br>    if (!alarm) throw new NotFoundException();<br>    alarm.isAcknowledged = true;<br>    return alarm;<br>  }<br>}</pre><p>Sound familiar? That’s the monolith AlarmList component all over again.</p><p><strong>Same problems:</strong></p><ul><li>Business logic stuck in the controller</li><li>Can’t reuse the acknowledge logic elsewhere</li><li>Hard to test without making HTTP calls</li><li>Can’t switch from in-memory to a real database without rewriting everything</li></ul><h3>How You’d Fix This in React</h3><p>Before fixing the backend, let’s see how you’d fix the React component:</p><pre>// ✅ GOOD: Separated concerns<br>// 1. Custom hook for data fetching (Service layer)<br>function useAlarms() {<br>  const [alarms, setAlarms] = useState([]);<br>  useEffect(() =&gt; {<br>    alarmAPI.getAll().then(setAlarms);<br>  }, []);<br>  const acknowledgeAlarm = async (id) =&gt; {<br>    await alarmAPI.acknowledge(id);<br>    setAlarms(alarms.map(a =&gt;<br>      a.id === id ? {...a, isAcknowledged: true} : a<br>    ));<br>  };<br>  return { alarms, acknowledgeAlarm };<br>}<br>// 2. Pure utility function (Domain logic)<br>function getSeverityColor(severity) {<br>  const colors = { high: &#39;red&#39;, medium: &#39;yellow&#39;, low: &#39;green&#39; };<br>  return colors[severity];<br>}<br>// 3. Presentational component (Controller)<br>function AlarmList() {<br>  const { alarms, acknowledgeAlarm } = useAlarms();<br>  // ... renders UI only<br>}<br>// 4. API client (Adapter)<br>const alarmAPI = {<br>  getAll: () =&gt; fetch(&#39;/api/alarms&#39;).then(r =&gt; r.json()),<br>  acknowledge: (id) =&gt; fetch(`/api/alarms/${id}/acknowledge`, { method: &#39;POST&#39; })<br>};</pre><p>Notice what happened:</p><ul><li>useAlarms hook can be used in multiple components</li><li>getSeverityColor is testable in isolation</li><li>alarmAPI can be swapped (fetch → axios → GraphQL)</li><li>AlarmList is clean and focused</li></ul><h3>Hexagonal Architecture Is Just This — With Backend Names</h3><p>Here’s the reveal. Every frontend pattern you just saw has an exact backend equivalent.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*56wwDnlJZAa74CosJq250g.gif" /></figure><p><em>Every row is a concept you already know. Only the name changed.</em></p><p>The mapping is precise:</p><p>ReactNestJS / HexagonalWhy they’re the sameComponentControllerBoth receive input, delegate work, never own business logicContext ProviderNestJS ModuleBoth inject dependencies into the scope belowCustom HookApplication ServiceBoth orchestrate without knowing implementation detailsRedux SliceDomain EntityBoth hold state transitions and pure business rulesZod / PropTypesValue ObjectBoth validate at the boundary and reject invalid inputTypeScript InterfacePort (abstract class)Both define a contract without implementationfetch / axiosAdapterBoth implement the contract defined above</p><h3>The Key Insight: One Arrow Points the Wrong Way</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VPUbSUQwJ3qhJpBJ9I1LzQ.gif" /></figure><p><em>Watch for the teal arrow. It’s the only one pointing up — and that’s the whole point.</em></p><p>The dependency rule in normal code flows downward: UI → Service → Database. Hexagonal breaks this at exactly one place.</p><p>The <strong>Application Service</strong> says: “I need a repository with these methods.” It defines the interface (Port). Then the database adapter <em>implements</em> that interface — the dependency points from infrastructure <strong>up toward</strong> the application.</p><p>This means:</p><ul><li>Your domain and application code never import from your database layer</li><li>You can swap databases without touching a single line of business logic</li><li>You can test the entire application layer with a fake in-memory adapter</li></ul><h3>Building It — Step by Step</h3><p>Here’s how the project structure emerges, one layer at a time:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FSRzMQ7zt7ZxGVjK2LixoA.gif" /></figure><p><em>Each folder is a layer. Each file lives exactly where its responsibility says it should.</em></p><p>The folder structure:</p><pre>src/alarms/<br>├── domain/<br>│   ├── value-objects/severity.vo.ts   ← Pure validation, zero deps<br>│   └── alarm.entity.ts                 ← Business rules (acknowledge, etc.)<br>├── application/<br>│   ├── ports/alarm-repository.port.ts  ← The contract (abstract class)<br>│   └── services/alarm.service.ts       ← Orchestrates domain + port<br>├── infrastructure/<br>│   ├── in-memory/in-memory-alarm.repository.ts<br>│   └── postgres/postgres-alarm.repository.ts<br>├── presenters/<br>│   └── http/alarms.controller.ts       ← Only file that knows about HTTP<br>└── alarms.module.ts                    ← Wires everything together</pre><h3>Step 2: The Domain Layer (Pure Business Rules)</h3><pre>// src/alarms/domain/value-objects/severity.vo.ts<br>export class Severity {<br>  private constructor(private readonly value: string) {}<br>  static create(value: string): Severity {<br>    const valid = [&#39;low&#39;, &#39;medium&#39;, &#39;high&#39;, &#39;critical&#39;];<br>    if (!valid.includes(value)) {<br>      throw new Error(`Severity must be one of: ${valid.join(&#39;, &#39;)}`);<br>    }<br>    return new Severity(value);<br>  }<br>  getValue(): string { return this.value; }<br>  isUrgent(): boolean { return this.value === &#39;high&#39; || this.value === &#39;critical&#39;; }<br>  getColor(): string {<br>    return { low: &#39;green&#39;, medium: &#39;yellow&#39;, high: &#39;orange&#39;, critical: &#39;red&#39; }[this.value];<br>  }<br>}</pre><p>This code has <strong>zero dependencies</strong>. No HTTP, no database, no NestJS. Copy it to any TypeScript project and it runs. That’s the domain layer.</p><h3>Step 3: The Entity</h3><pre>// src/alarms/domain/alarm.entity.ts<br>export class Alarm {<br>  private isAcknowledged: boolean = false;<br>  constructor(<br>    public readonly id: string,<br>    public readonly name: string,<br>    public readonly severity: Severity,<br>    public readonly facilityId: string,<br>    public readonly createdAt: Date = new Date()<br>  ) {}<br>  acknowledge(): void {<br>    if (this.isAcknowledged) {<br>      throw new Error(&#39;Alarm already acknowledged&#39;); // Business rule!<br>    }<br>    this.isAcknowledged = true;<br>  }<br>  requiresImmediateAttention(): boolean {<br>    return this.severity.isUrgent() &amp;&amp; !this.isAcknowledged;<br>  }<br>  getStatus(): &#39;pending&#39; | &#39;acknowledged&#39; {<br>    return this.isAcknowledged ? &#39;acknowledged&#39; : &#39;pending&#39;;<br>  }<br>}</pre><h3>Step 4: The Port (The Contract)</h3><pre>// src/alarms/application/ports/alarm-repository.port.ts<br>// Abstract class = interface that survives TypeScript compilation<br>// NestJS needs classes for dependency injection<br>export abstract class AlarmRepository {<br>  abstract save(alarm: Alarm): Promise&lt;void&gt;;<br>  abstract findAll(): Promise&lt;Alarm[]&gt;;<br>  abstract findById(id: string): Promise&lt;Alarm | null&gt;;<br>  abstract findByFacility(facilityId: string): Promise&lt;Alarm[]&gt;;<br>}</pre><p>This is the React equivalent of defining interface DataService before implementing it. Your components depend on the interface. Then you pass either Firebase or your custom backend.</p><h3>Step 5: The Application Service</h3><pre>// src/alarms/application/services/alarm.service.ts<br>@Injectable()<br>export class AlarmService {<br>  // Depends on the ABSTRACTION (port), not the implementation<br>  constructor(private readonly alarmRepository: AlarmRepository) {}<br>  async createAlarm(input: {<br>    name: string; severity: string; facilityId: string;<br>  }): Promise&lt;Alarm&gt; {<br>    const severity = Severity.create(input.severity);  // Domain validation<br>    const alarm = new Alarm(Date.now().toString(), input.name, severity, input.facilityId);<br>    await this.alarmRepository.save(alarm);            // Uses the port<br>    return alarm;<br>  }<br>  async acknowledgeAlarm(id: string): Promise&lt;void&gt; {<br>    const alarm = await this.alarmRepository.findById(id);<br>    if (!alarm) throw new Error(&#39;Alarm not found&#39;);<br>    alarm.acknowledge();                               // Business rule from domain<br>    await this.alarmRepository.save(alarm);<br>  }<br>}</pre><p>Notice what’s <strong>NOT</strong> here: no @Post(), no HTTP status codes, no database queries.</p><h3>Steps 6 &amp; 7: Two Adapters, One Interface</h3><pre>// In-memory (for development &amp; testing)<br>@Injectable()<br>export class InMemoryAlarmRepository extends AlarmRepository {<br>  private alarms = new Map&lt;string, Alarm&gt;();<br>  async save(alarm: Alarm): Promise&lt;void&gt; { this.alarms.set(alarm.id, alarm); }<br>  async findAll(): Promise&lt;Alarm[]&gt; { return Array.from(this.alarms.values()); }<br>  async findById(id: string): Promise&lt;Alarm | null&gt; { return this.alarms.get(id) || null; }<br>  async findByFacility(facilityId: string): Promise&lt;Alarm[]&gt; {<br>    return (await this.findAll()).filter(a =&gt; a.facilityId === facilityId);<br>  }<br>}<br>// PostgreSQL (for production)<br>@Injectable()<br>export class PostgresAlarmRepository extends AlarmRepository {<br>  constructor(<br>    @InjectRepository(AlarmEntity)<br>    private readonly repo: Repository&lt;AlarmEntity&gt;<br>  ) { super(); }<br>  async save(alarm: Alarm): Promise&lt;void&gt; {<br>    await this.repo.save(this.toEntity(alarm));<br>  }<br>  // ... same interface, different engine<br>}</pre><h3>Step 8: The Controller</h3><pre>// src/alarms/presenters/http/alarms.controller.ts<br>@Controller(&#39;alarms&#39;)<br>export class AlarmsController {<br>  constructor(private readonly alarmService: AlarmService) {}<br>  @Post()<br>  async create(@Body() body: CreateAlarmDto) {<br>    const alarm = await this.alarmService.createAlarm(body);<br>    return {<br>      id: alarm.id,<br>      severity: alarm.severity.getValue(),<br>      severityColor: alarm.severity.getColor(),<br>      status: alarm.getStatus(),<br>    };<br>  }<br>  @Patch(&#39;:id/acknowledge&#39;)<br>  async acknowledge(@Param(&#39;id&#39;) id: string) {<br>    await this.alarmService.acknowledgeAlarm(id);<br>    return { message: &#39;Alarm acknowledged successfully&#39; };<br>  }<br>}</pre><h3>The Payoff — One Line Swaps the Database</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aG1qC_xblos6EfB5xhnFvA.gif" /></figure><p><em>Watch the toggle flip. Watch nothing else change.</em></p><pre>// src/alarms/alarms.module.ts<br>@Module({<br>  controllers: [AlarmsController],<br>  providers: [<br>    AlarmService,<br>    {<br>      provide: AlarmRepository,<br>      useClass: InMemoryAlarmRepository, // ← Change ONLY this line<br>      // useClass: PostgresAlarmRepository,<br>    },<br>  ],<br>})<br>export class AlarmsModule {}</pre><p>That’s it. One line. Controller, Service, Domain — none of them know which database you’re using.</p><p>Your frontend doesn’t know the difference either. GET /alarms returns the same shape regardless.</p><h3>When Should You Use This Pattern?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D_9XCOKIM6kzU885gZa5Nw.gif" /></figure><p>The short version:</p><p><strong>Use Hexagonal when:</strong></p><ul><li>You might switch databases (SQL → NoSQL, adding Redis)</li><li>You support multiple APIs (REST + GraphQL + WebSocket)</li><li>The team is large enough that boundaries help coordination</li><li>Business logic must be testable without a real database</li></ul><p><strong>Skip it when:</strong></p><ul><li>It’s a simple CRUD app with no complex rules</li><li>You’re building a prototype that needs to move fast</li><li>You’re certain the infrastructure will never change</li><li>The team is one or two people on a short-lived project</li></ul><p>The default should always be simplicity. Hexagonal is there when complexity earns it.</p><h3>Part 1 Summary</h3><p>You’ve learned that Hexagonal Architecture is not a foreign concept. It’s the same separation-of-concerns pattern you already use in React — just with backend names.</p><p>What you knewWhat you learnedComponentControllerCustom HookApplication ServiceRedux SliceDomain EntityTypeScript InterfacePortfetch / axiosAdapterContext ProviderNestJS Module</p><p>The key insight: <strong>the application defines what it needs (Port), infrastructure provides it (Adapter). The dependency arrow points up — on purpose.</strong></p><h3>What’s Coming in Part 2</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nM6bkVFOwzibWyLEgbIT1g.gif" /></figure><p>In Part 2 we build on this foundation:</p><ul><li><strong>Commands</strong> — like Redux actions, but for the backend write side</li><li><strong>Queries</strong> — like Reselect selectors, optimised for reads</li><li><strong>Events</strong> — like DOM events, but across your whole system</li><li><strong>Sagas</strong> — like useEffect with complex timing — we&#39;ll detect &quot;3 alarms in 5 seconds&quot; and notify a supervisor without the alarm creation code knowing anything about notifications</li></ul><p><em>Did this help? Clap if you want Part 2!</em></p><p><em>Questions? Drop them in the comments. I read every single one.</em></p><h3>Resources</h3><ul><li><a href="https://docs.nestjs.com">NestJS Documentation</a></li><li><a href="https://alistair.cockburn.us/hexagonal-architecture/">Hexagonal Architecture — Alistair Cockburn</a></li></ul><blockquote><strong><em>Series navigation:</em></strong><em> </em><strong><em>Part 1: Hexagonal Architecture</em></strong><em> ← you are here Part 2: CQRS &amp; Event-Driven Architecture </em>(coming soon)<em> Part 3: Event Sourcing </em>(coming soon)</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3be4664c2316" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/from-react-to-nestjs-master-a-frontend-engineers-journey-to-backend-architecture-3be4664c2316">From React to NestJS Master: A Frontend Engineer’s Journey to Backend Architecture (Part 1)</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How React Works (Part 9)? The Complete Picture: From a Keystroke to a Pixel]]></title>
            <link>https://blog.stackademic.com/how-react-works-part-9-the-complete-picture-from-a-keystroke-to-a-pixel-d68bbfb8db08?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/d68bbfb8db08</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react-hook]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[react-architecture]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Thu, 07 May 2026 12:52:14 GMT</pubDate>
            <atom:updated>2026-05-07T13:32:33.488Z</atom:updated>
            <content:encoded><![CDATA[<h3>The Complete Picture: From a Keystroke to a Pixel</h3><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>This is the final article.</em></strong><em> If you haven’t read Parts 1–8, this article won’t land the way it should. Go back and start there.</em></blockquote><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>Part 1:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-1-motivation-behind-react-fiber-time-slicing-suspense-062e6431d4b0"><em>Motivation Behind React Fiber: Time Slicing &amp; Suspense</em></a><em> </em><strong><em>Part 2:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-98e2f10ec0a7"><em>Why React Had to Build Its Own Execution Engine</em></a><em> </em><strong><em>Part 3:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-3-how-react-finds-what-actually-changed-35680df98712"><em>How React Finds What Actually Changed</em></a><em> </em><strong><em>Part 4:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97"><em>The Idea That Makes Suspense Possible</em></a><em> </em><strong><em>Part 5:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-25afdf54305b"><em>The React Lifecycle From the Inside</em></a><em> </em><strong><em>Part 6:</em></strong><em> </em><a href="https://medium.com/@samabaasi/how-react-works-part-6-how-state-actually-works-usestate-from-the-inside-51e2d785a420"><em>How State Actually Works</em></a><em> </em><strong><em>Part 7:</em></strong><em> </em><a href="https://medium.com/@samabaasi/how-react-works-part-7-the-trap-of-vibe-coding-usecallback-e5548006b320"><em>The Trap of Vibe Coding useCallback</em></a><em> </em><strong><em>Part 8:</em></strong><em> </em><a href="https://medium.com/@samabaasi/how-react-works-part-8-server-components-hydration-the-real-story-65f66b4d7bb1"><em>Server Components &amp; Hydration: The Real Story</em></a></blockquote><h3>Eight Articles. One Question.</h3><p>In Part 1, Dan Abramov stood in front of a room in Iceland and asked:</p><blockquote><strong><em>“With vast differences in computing power and network speed, how do we deliver the best user experience for everyone?”</em></strong></blockquote><p>We’ve spent eight articles building the answer. But we’ve never seen all the pieces working together at once. We’ve seen Fiber explained. The Scheduler explained. The Reconciler. Algebraic effects. Lifecycle. State. Performance. Server Components. Each one in isolation.</p><p>This article puts them together. Not as a summary — as a story. One user. One interaction. Every layer of React underneath it.</p><p>By the end, you’ll see that everything we built across eight articles was always one system.</p><h3>The User</h3><p>She opens a product search page on a mid-range Android phone — the kind of device hundreds of millions of people actually use. Her connection is decent but not fast. She’s looking for headphones.</p><p>She types. She clicks. She adds something to her cart.</p><p>From her perspective: the page loads, she types, results appear, she clicks a button, the cart updates. Four seconds of her life.</p><p>Underneath those four seconds, every system in this series ran. Let’s trace it.</p><h3>The Page Loads</h3><p>The request hits the server. This is Part 8’s world.</p><p>Server Components run — async, directly querying the database. The product catalog, the navigation, the page layout. None of this code ships to her phone. It renders on the server and becomes the <strong>RSC payload</strong> — a JSON description of the UI that streams to the client in chunks. Her phone gets real HTML almost immediately, not a white screen waiting for JavaScript.</p><p>While the HTML is visible, React starts hydration in the background. It walks the server-rendered DOM and matches each node to its Fiber counterpart — attaching event listeners, storing references, making the page interactive. Because React 18 uses the same Lane priority system from Part 3, if she taps something before hydration finishes, React jumps to that boundary first. The rest of the tree waits. Her tap doesn’t go ignored.</p><p>The page is ready. She types.</p><h3>She Types “h”</h3><p>This is where Parts 2, 3, and 6 all meet.</p><p>A change event fires. React calls setQuery(&#39;h&#39;). Calling setQuery doesn&#39;t immediately update anything — it creates an update object, drops it into a global buffer, and marks a trail called childLanes on every ancestor of the search input fiber, all the way to the root. This is how React knows where work is waiting without walking the entire tree.</p><p>The Scheduler sees the update. Because this is a user interaction, it’s <strong>SyncLane</strong> — the highest priority. It runs synchronously. The call stack equivalent of &quot;drop everything and handle this.&quot;</p><p>React walks the Fiber tree following the trail of childLanes. Every branch with childLanes === 0 is skipped instantly — the footer, the recommendations panel, the filters sidebar. React touches only what needs to change. The input updates. The character appears on screen.</p><p>That took a few milliseconds. She doesn’t notice. She keeps typing.</p><h3>The Results Need to Update</h3><p>Here’s the problem that Part 1 opened with — still alive, still relevant.</p><p>Filtering 10,000 products on every keystroke is expensive. If React treated the results update with the same urgency as the input update, every character she types would block until the filtering finished. The input would lag. The experience would feel broken.</p><p>This is exactly Dan’s typing + chart demo from 2018. The chart is the results list. It has to update. But it shouldn’t block the input.</p><p>The fix is startTransition — wrapping the expensive update so React knows it can wait. The input still updates immediately at SyncLane. The results update in the background at TransitionLane, interruptibly:</p><pre>setQuery(e.target.value);                          // urgent — runs now<br>startTransition(() =&gt; setResults(filter(query)));  // deferrable — runs when free</pre><p>That’s the entire mechanism. startTransition tells React: this update is real but not urgent. Internally, React assigns it a TransitionLane instead of SyncLane. The Scheduler now has two tasks — the keystroke at the top, the results render below it.</p><p>The results render begins using the concurrent work loop — the one that checks shouldYield() after every fiber. She types another character. A new SyncLane keystroke arrives. React <strong>abandons the results render mid-tree</strong>, handles the keystroke — she sees it immediately — then starts the results render again with the new query.</p><p>isPending tells you the transition is in progress, so you can show a spinner or dim the old results while new ones load. The old results stay visible until the render completes. She never sees a blank list.</p><p>This is the Git metaphor from Part 1 made real: the keystroke is always the hotfix on main. The results render is always the feature branch. React rebases automatically.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8rtkG2hI1PL3kPTUckblJQ.png" /></figure><p><strong>What if you can’t wrap the setter?</strong></p><p>Sometimes the state lives in a child component or a library you don’t control. useDeferredValue solves this — it gives you a deferred copy of a value that React renders at lower priority, interruptibly, without you needing to touch the setter:</p><pre>const deferredQuery = useDeferredValue(query);<br>// ResultList receives deferredQuery — renders at TransitionLane, same mechanism</pre><p>The practical rule: use useTransition when you own the setter, useDeferredValue when you own the value but not where it comes from.</p><h3>React Finds What Changed</h3><p>She paused typing. The results render runs to completion. This is Part 3.</p><p>React walks the ResultList fiber, runs the component function, gets new JSX. The Reconciler compares old to new. Each product has a key prop — its ID. React builds a map of old fibers keyed by product ID. For each new product, it looks up the existing fiber. Found: reuse it, update only what changed. Not found: create it. Products no longer in results: mark for deletion.</p><p>React doesn’t touch the DOM during any of this. It marks fibers with flags — insert this, update that, delete this — and bubbles those flags up the tree. By the time it reaches the root, every decision about what needs to change is recorded. Nothing has changed in the DOM yet.</p><h3>The DOM Updates</h3><p>The Commit phase runs — synchronous, uninterruptible. The two Fiber trees swap. The workInProgress tree becomes current. What was being built in the background is now the source of truth.</p><p>DOM operations execute in order: deletions first, then insertions, then updates. Products that disappeared are removed. New products are inserted. Changed cards get their props updated directly on the existing DOM node — no recreation.</p><p>The browser paints. She sees the filtered results.</p><h3>She Clicks “Add to Cart”</h3><p>The click fires. setCartCount(c =&gt; c + 1) is called. Same machinery as the keystroke — SyncLane, update queued, trail marked up to the root, Scheduler fires synchronously.</p><p>React walks the trail. Everything outside the Header subtree has childLanes === 0 — the results list, the filters, the footer — all skipped in one check each. React touches only the Header and its CartIcon child.</p><p>The render runs. The commit runs. Now Part 5 takes over.</p><p>useLayoutEffect fires synchronously — the DOM is updated, the browser hasn&#39;t painted yet. If CartIcon needs to measure its width for an animation, this is the only safe moment. Then the browser paints. She sees the cart count increment.</p><p>In the next macro task — after paint, via MessageChannel — useEffect runs. The analytics event fires. localStorage syncs. None of this blocked the paint. None of it delayed what she saw.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HIx2kimTF3Ho4WwHgtWP_A.png" /></figure><h3>What Didn’t Run</h3><p>Of the entire component tree — dozens of components, hundreds of fibers — exactly three ran their render functions for the cart click:</p><ul><li>The root — has childLanes &gt; 0</li><li>Header — has childLanes &gt; 0</li><li>CartIcon — the state changed here</li></ul><p>Everything else: ResultList, SearchInput, Filters, Footer, Recommendations, every ProductCard — skipped. Not because of React.memo. Not because of useCallback. Because childLanes was zero on every other subtree, and React&#39;s built-in bailout skipped each one in a single check.</p><p>This is the answer to Part 7. The tools exist for the rare cases where the built-in bailout genuinely isn’t enough. For everything else — the default behavior handles it.</p><h3>The Complete Map</h3><p>Eight parts. One answer.</p><p><strong>Fiber</strong> is the foundation. React replaced the opaque JavaScript call stack with its own — plain objects it controls completely. This made pausing, resuming, and prioritizing possible. Without Fiber, nothing else in this series exists.</p><p><strong>The Scheduler</strong> sits on top of Fiber and decides what runs and when. Keystrokes get SyncLane — they always run first, synchronously. Results renders get TransitionLane — they run in the background, interruptibly. shouldYield() checks the clock after every fiber to keep the browser breathing between frames.</p><p><strong>The Reconciler</strong> uses the Fiber tree to find the minimum set of DOM changes. It follows the childLanes trail to find what changed, skips everything with clean subtrees, diffs lists by key, and marks flags on fibers — never touching the DOM until the entire decision is made. Then the Commit phase applies everything atomically so the user never sees a half-updated UI.</p><p><strong>Algebraic Effects</strong> is the mental model that connects Suspense, ErrorBoundary, and hooks. Components signal what they need by throwing. React — acting as the effect handler — catches it, decides what to do, and resumes when the data arrives. useState feels local because of this model, even though state lives on a Fiber node React controls.</p><p><strong>Lifecycle</strong> is when effects run relative to the Commit phase. useLayoutEffect fires synchronously before the browser paints — for DOM measurements that must happen before the user sees anything. useEffect fires after paint in the next macro task — for everything else. The separation isn&#39;t arbitrary. It maps directly to the Commit phase structure.</p><p><strong>State</strong> is a hook object on a Fiber’s linked list. Calling setState doesn&#39;t update immediately — it drops a note in a queue. React reads that queue during the next render, on its own schedule, in its own order. This is why you see the old value after calling setState. The snapshot hasn&#39;t changed yet. The note is pending.</p><p><strong>Performance</strong> is mostly free. The childLanes bailout skips entire subtrees in one check when nothing changed. Structural patterns like children-as-props prevent unnecessary re-renders without any memoization overhead. React.memo, useCallback, and useMemo exist for the small set of cases where the built-in bailout genuinely isn&#39;t enough — and they should only be added after measuring.</p><p><strong>Server Components</strong> moved the question from “how do we render faster on the client” to “how do we send less to the client in the first place.” Server code that never ships. RSC payloads that stream progressively. Selective hydration that prioritizes what the user is interacting with. Every Server Action a public endpoint — treat it like one.</p><h3>What’s Still Being Built</h3><p>The series ends here but React doesn’t.</p><p><strong>The React Compiler</strong> is stable and shipping. It automatically applies the memoization from Part 7 at build time — React.memo, useMemo, useCallback — applied intelligently, without you writing any of it. Understanding this series is exactly what makes your code Compiler-friendly.</p><p><strong>use()</strong> is the ergonomic layer on top of the algebraic effects model from Part 4. Where Suspense required a data library to implement the throw-Promise pattern, use() exposes it directly from React.</p><p><strong>Server Actions security</strong> is still maturing. React2Shell revealed a persistent gap between the mental model (local function) and the reality (public HTTP endpoint). The ecosystem is figuring out better defaults. Until then: validate everything.</p><h3>The One-Sentence Answer</h3><p>React delivers the best user experience for everyone by giving developers a declarative component model while handling — invisibly — the complexity of prioritizing work, interrupting expensive renders, deferring non-urgent updates, minimizing DOM changes, and progressively delivering content from server to client, through the Fiber and Scheduler machinery built for exactly this purpose.</p><p>Eight articles to earn one sentence.</p><h3>What You Actually Have Now</h3><p>Before this series, React was a tool you used. After it, React is a system you understand.</p><p>The next time an input lags, you know it’s a SyncLane update blocked by a lower-priority render — and you know startTransition is the right fix, not useCallback. The next time a component re-renders unexpectedly, you know to check the reference equality of its props and the childLanes trail — not to wrap everything in React.memo. The next time a page loads slowly, you know the difference between a bundle size problem (Server Components), a data waterfall problem (streaming and Suspense), and a render performance problem (Fiber and Reconciler).</p><p>The tools haven’t changed. But the questions you ask before reaching for them have.</p><p>That’s what understanding internals actually gives you: not the ability to contribute to React’s source code, but the ability to debug faster, make better decisions earlier, and stop adding complexity to fix problems you only half-understood.</p><p>Write something. Ship it. Now you’ll know why it works.</p><h3>🙏 Thank You</h3><p>This series exists because of the people who built these systems and took the time to explain them.</p><p><strong>Dan Abramov</strong> — for Beyond React 16, the Git metaphor that makes Time Slicing click, RSC from Scratch, Algebraic Effects for the Rest of Us, and overreacted.io. More teaching in one career than most people manage in ten.</p><p><strong>Andrew Clark</strong> — for the react-fiber-architecture document that set the design goals every article in this series built toward.</p><p><strong>Sebastian Markbåge</strong> — for the algebraic effects mental model that connects Suspense, ErrorBoundary, Hooks, and Context into one coherent story.</p><p><strong>Matheus Albuquerque</strong> — for the clearest explanation of FiberNode internals at React Summit 2022.</p><p><strong>Sam Galson</strong> — for connecting React to the wider computer science of fibers, coroutines, and continuations.</p><p><a href="https://jser.dev"><strong>JSer (jser.dev)</strong></a> — for the deepest source-level analysis of React internals anywhere on the internet. Every code-level claim in this series was verified against JSer’s work. An extraordinary resource.</p><p><strong>Sophie Alpert, Joe Savona, Luna Wei</strong> — for the React team’s continued work on documentation, RFCs, and honest discussion of tradeoffs.</p><p><a href="https://www.lydiahallie.io/"><strong>Lydia Hallie</strong></a> — for JavaScript visualizations that shaped this series’ style from the beginning.</p><p><a href="https://github.com/lachlan2k"><strong>Lachlan Davidson</strong></a> — for responsibly disclosing CVE-2025–55182 and working with the React team to fix it.</p><p><em>That’s the series. Thank you for reading. 🔧</em></p><p><strong>Tags:</strong> #react #javascript #webdev #tutorial</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d68bbfb8db08" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/how-react-works-part-9-the-complete-picture-from-a-keystroke-to-a-pixel-d68bbfb8db08">How React Works (Part 9)? The Complete Picture: From a Keystroke to a Pixel</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How React Works (Part 8)? Server Components & Hydration: The Real Story]]></title>
            <link>https://blog.stackademic.com/how-react-works-part-8-server-components-hydration-the-real-story-65f66b4d7bb1?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/65f66b4d7bb1</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[react-server-component]]></category>
            <category><![CDATA[hydration]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Wed, 06 May 2026 11:45:41 GMT</pubDate>
            <atom:updated>2026-05-07T13:32:22.613Z</atom:updated>
            <content:encoded><![CDATA[<h3>Server Components &amp; Hydration: The Real Story</h3><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>Part 1:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-1-motivation-behind-react-fiber-time-slicing-suspense-062e6431d4b0"><em>Motivation Behind React Fiber: Time Slicing &amp; Suspense</em></a><em> </em><strong><em>Part 2:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-98e2f10ec0a7"><em>Why React Had to Build Its Own Execution Engine</em></a><em> </em><strong><em>Part 3:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-3-how-react-finds-what-actually-changed-35680df98712"><em>How React Finds What Actually Changed</em></a><em> </em><strong><em>Part 4:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97"><em>The Idea That Makes Suspense Possible</em></a><em> </em><strong><em>Part 5:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-25afdf54305b"><em>The React Lifecycle From the Inside</em></a><em> </em><strong><em>Part 6:</em></strong><em> </em><a href="https://medium.com/@samabaasi/how-react-works-part-6-how-state-actually-works-usestate-from-the-inside-51e2d785a420"><em>How State Actually Works</em></a><em> </em><strong><em>Part 7:</em></strong><em> </em><a href="https://medium.com/@samabaasi/how-react-works-part-7-the-trap-of-vibe-coding-usecallback-e5548006b320"><em>The Trap of Vibe Coding useCallback</em></a><em> </em><strong><em>Prerequisites:</em></strong><em> Read Parts 1–7 first.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qdffSdTi2iZSw929YtZIEQ.png" /><figcaption><em>How React Works Under the Hood</em></figcaption></figure><h3>Where We Left Off</h3><p>Through Parts 1–7, every problem we solved lived on the client. The call stack couldn’t pause — so React built Fiber. Re-rendering was too slow — so React built the Reconciler. State management was confusing — because it’s a queue on a Fiber node, not a variable. Performance tools were overused — because developers reached for them before understanding the problem.</p><p>Every solution stayed inside the browser. The user’s device was the bottleneck. The network was just the delivery mechanism.</p><p>Part 8 changes that. Because there’s a whole category of problem that no client-side optimization can fix — and it took the React team until 2023 to fully solve it.</p><h3>The Problem Nobody Talks About</h3><p>You’ve shipped a React app. You’ve done everything right. You memoized where it mattered. You fixed re-renders. You code-split your routes. React DevTools shows a clean profile. Your API is fast.</p><p>Then you open Chrome on a mid-range Android phone with a throttled 3G connection — the kind of device hundreds of millions of people actually use — and you watch the screen stay white for four seconds before anything appears.</p><p>You look at the Network tab. Before your user sees a single pixel of your app, their phone downloaded, parsed, and executed 380kb of JavaScript. You open the bundle analyzer. Half of that JavaScript is components that render completely static content — a product description that never changes, a nav with hardcoded links, a footer, a terms page. None of them use state. None of them handle events. They just render text and markup.</p><p>But they ship to every client. On every load. On every device. Including that phone on 3G that Part 1 opened with.</p><p>This is the problem that Part 1’s question — <em>“with vast differences in computing power and network speed, how do we deliver the best user experience for everyone?”</em> — still hadn’t fully answered by the end of Part 7. We optimized what happens on the client. But we never questioned why so much work was happening on the client in the first place.</p><p>The naive fix was server-side rendering. But SSR had a fundamental flaw that took years to properly name — and understanding that flaw is what makes Server Components make sense.</p><h3>The Problem: The Two-Trip Lie</h3><p>To understand why Server Components exist, you need to go back to how server rendering worked before them.</p><p>Dan Abramov explained the origin story in his RSC from Scratch deep dive by taking you back to 2003. PHP on a server: the server gets a request, reads a file or a database, returns HTML. Simple. The user sees content immediately. No JavaScript bundle to download. No client-side rendering. Just a server doing work and returning a result.</p><p>React brought rich interactivity but also a cost: everything runs in the browser. Large bundles. Slow initial loads on slow connections. So the industry added server rendering back — render HTML on the server first, send it to the client, then download JavaScript and “hydrate” the page to make it interactive.</p><p>This solved the blank screen problem. But it created what the React team calls a <strong>two-trip problem</strong>.</p><p>The server renders HTML — fast, visible immediately. Then the browser downloads all the JavaScript anyway. React runs again on the client. Re-does the work. Attaches event listeners. Any data fetched on the server to render the HTML? Fetched again on the client. Bundle size? Unchanged — every component that ran on the server also ships to the client. Server work didn’t reduce client work. It looked like a performance win but the full cost came later.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-mpffF45tWToD9Wr1Yg4Tw.png" /></figure><p>This is the problem Server Components actually solve.</p><h3>What Hydration Actually Is — and What’s Wrong With It</h3><p>Server-side rendering gave React apps faster first paint. But it introduced a problem that almost nobody explained clearly: <strong>the page looks ready before it is.</strong></p><p>The server renders HTML and sends it. The user sees a complete-looking page — the nav, the content, the buttons. They try to click something. Nothing happens. The button doesn’t respond. The form doesn’t submit. The dropdown doesn’t open. The page looks interactive but it isn’t — because React hasn’t attached any event listeners yet.</p><p>This is the <strong>hydration gap</strong>. And it exists because of how hydration works.</p><p>From jser.dev’s hydration internals: hydration is not re-rendering. React does not recreate the DOM. Instead, React walks the existing server-rendered DOM tree in parallel with the Fiber tree it’s building, matching each fiber to its corresponding DOM node — storing a reference in fiber.stateNode. Event listeners are attached. The DOM is reused. This is why hydration is faster than a fresh render.</p><p>But the entire tree must hydrate before any part becomes interactive. One slow component at the top of the tree blocks the click handlers on a button at the bottom. React walks the tree sequentially, and until that walk is complete, nothing responds.</p><p>React 18 fixed this with <strong>selective hydration</strong>. Using the same Lanes priority system from Part 3, React can now interrupt hydration to handle a user interaction first. If a user clicks a button before hydration reaches it, React jumps to that boundary using SyncLane — hydrates just enough to handle the interaction — then resumes the rest of the tree. The architecture from Part 3 extends directly here: interruptible work, priority-driven, same system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TgqhxuTUweod5GOGHrP2-A.png" /></figure><p>Streaming takes this further still. Instead of waiting for all data before sending any HTML, the server sends the shell immediately and streams each Suspense boundary as its data resolves. The client starts hydrating what arrived while the rest is still coming. Multiple chunks, progressive — no blank screen waiting for the slowest database query.</p><p>But even with selective hydration and streaming, there’s still a cost that SSR never eliminated. The JavaScript still ships. Every component still runs on the client. That’s the problem Server Components solve.</p><h3>What Server Components Actually Are</h3><p>From the React Labs March 2023 blog post, written by the React team:</p><blockquote>“We are introducing a new kind of component — Server Components — that run ahead of time and are excluded from your JavaScript bundle. Server Components can run during the build, letting you read from the filesystem or fetch static content. They can also run on the server, letting you access your data layer without having to build an API.”</blockquote><p>Server Components run only on the server. Their code never ships to the client. Client Components — marked explicitly with &#39;use client&#39; — run on the client as before. The boundary between them is a decision you make explicitly at every component.</p><p>The critical constraint the boundary enforces: <strong>functions cannot cross from Server to Client.</strong> Functions aren’t serializable. If you try to pass a function as a prop from a Server Component to a Client Component, React throws an error. Only plain serializable values can cross — strings, numbers, objects, arrays, React elements. This shapes every RSC architecture decision. It’s why you can’t pass event handlers from the server side.</p><p>What the server sends is not HTML and not JavaScript. It’s the <strong>RSC payload</strong> — a JSON-like description of the rendered UI tree. Here’s what a simple RSC payload actually looks like in practice:</p><pre>J0:[&quot;$&quot;,&quot;div&quot;,null,{&quot;className&quot;:&quot;post&quot;}]<br>J1:[&quot;$&quot;,&quot;h1&quot;,null,{&quot;children&quot;:&quot;Hello World&quot;}]<br>J2:[&quot;$&quot;,&quot;p&quot;,null,{&quot;children&quot;:&quot;Content here&quot;}]</pre><p>Each line is a serialized React element — type, key, ref, props. React on the client reads this and builds or updates its Fiber tree from it. As Dan’s RSC from Scratch describes: <em>“a production-ready RSC setup sends JSX chunks as they’re being produced instead of a single large blob at the end. When React loads, hydration can start immediately — React starts traversing the tree using the JSX chunks that are already available.”</em></p><p>For Client Components, only their props get serialized into the payload — not their code, which stays in the bundle. The payload contains a reference to the Client Component (a module ID the bundler knows about) plus the props to render it with.</p><p>This is why RSC works with client-side navigation: navigating to a new page in a Next.js App Router app doesn’t reload the whole page. It fetches a new RSC payload and React merges it into the existing Fiber tree — same mechanism, different data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3Cb2bA3unYm5JkYddR-xaA.png" /></figure><p>The React docs explain this model clearly — the boundary rules, how composition works, how to think about what goes where. It’s genuinely good documentation.</p><p>What the docs didn’t prepare developers for: the mental model they encourage — writing async function submitOrder() marked &#39;use server&#39; and calling it like any local function — is exactly the mental model that skips the input validation you&#39;d never skip on an API route. In December 2025, that gap cost real companies their servers.</p><h3>The Security Reality: React2Shell</h3><p>On November 29th, 2025, security researcher Lachlan Davidson reported a vulnerability to Meta’s Bug Bounty program. Four days later, the React team disclosed it publicly.</p><p>From the official React blog post:</p><blockquote>“There is an unauthenticated remote code execution vulnerability in React Server Components.”</blockquote><p><strong>CVE-2025–55182 — CVSS 10.0.</strong></p><p>The root cause: React Server Functions (Server Actions) allow a client to call a function on the server. React translates the client’s request into an HTTP request, which the server deserializes and executes. The deserialization process did not properly validate inputs. An attacker could craft a malicious HTTP request that, when deserialized by React, achieved remote code execution on the server — <strong>with no authentication required, against any app using React Server Components.</strong></p><p>The React blog was direct: <em>“Even if your app does not implement any React Server Function endpoints it may still be vulnerable if your app supports React Server Components.”</em></p><p>Exploits appeared within 24 hours of disclosure. Unit 42 at Palo Alto Networks documented post-exploitation activity including reverse shells, malware installation, and activity linked to state-sponsored threat actors.</p><p>Two follow-on CVEs were disclosed days later: source code exposure (<strong>CVE-2025–55183</strong> — a crafted request could cause the server to return its own source code) and denial of service (<strong>CVE-2025–55184</strong> — a crafted request caused an infinite loop that hung the server process permanently). The initial fix for the DoS was incomplete, requiring a second patch under <strong>CVE-2025–67779</strong>. A fourth vulnerability was disclosed in January 2026 (<strong>CVE-2026–23864</strong>).</p><p><strong>What this means for you as a developer:</strong></p><p>Every Server Action (&#39;use server&#39; function) is a public HTTP endpoint. Anyone on the internet can send a POST request to it — not just your frontend. There is no framework magic that makes inputs safe by default. You must validate and sanitize every argument exactly as you would validate input to an Express route handler or an API endpoint.</p><p>The mistake RSC encourages is writing server-side logic that <em>feels</em> like an internal function — you’re just calling submitForm() from a button click, it looks local. But it&#39;s an HTTP endpoint. Untrusted input. Treat it that way.</p><p>Additionally: whatever you pass as props from a Server Component to a Client Component travels in the RSC payload — visible in the browser’s network tab. Pass only what the client needs to render. Passing an entire database row when only a name is needed exposes the rest of that data to anyone watching the network response.</p><h3>A Concrete Trace: What Actually Happens on Navigation</h3><p>Let’s make this real. When a user clicks a link in a Next.js App Router app:</p><p><strong>User clicks a link.</strong> React intercepts the navigation. Instead of a full page reload, it fetches the RSC payload for the new route from the server.</p><p><strong>Server processes the request.</strong> Server Components for the new route run — async, querying databases, reading files. They render to the RSC payload format. Client Component boundaries are replaced with module references plus serialized props.</p><p><strong>Payload streams to the client.</strong> The response arrives in chunks. React starts processing chunks as they arrive — it doesn’t wait for the complete response.</p><p><strong>React merges the payload into the Fiber tree.</strong> For unchanged parts (shared layout, nav), React skips re-rendering entirely. For new content, React creates or updates fibers from the payload. Client Components receive their serialized props and render on the client.</p><p><strong>The page updates.</strong> No full reload. No loss of client-side state. The URL changes. The content changes. Client Components that were already hydrated stay hydrated.</p><p>This is the genuine architectural win: <strong>the server does the data work, the client does the interaction work, and the payload is the bridge between them.</strong></p><h3>The Vibe Coding RSC Trap</h3><p>In Part 7 we saw what happens when developers add useCallback and React.memo everywhere without measuring — code that&#39;s harder to read, harder to debug, and often slower than before.</p><p>The same pattern exists with Server Components. You read the Next.js App Router docs. You migrate your pages directory. You follow the examples. And you end up writing code like this:</p><pre>// Every component that uses state or events gets &#39;use client&#39;<br>&#39;use client&#39;;<br>export function Header() { ... }<br><br>&#39;use client&#39;;<br>export function SearchInput() { ... }<br>&#39;use client&#39;;<br>export function ProductCard() { ... }<br>&#39;use client&#39;;<br>export function Footer() { ... } // doesn&#39;t even use state - added just in case</pre><p>Every component is a Client Component. Every component ships JavaScript to the client. The RSC architecture is in place — the routing, the layout, the page files — but the actual benefit (JavaScript that never ships) is zero. You’ve added the complexity of the Server/Client boundary, the mental overhead of &#39;use client&#39; everywhere, and the security surface of Server Actions — for a bundle size identical to before.</p><p>This happens because the docs show you the mechanics but not the principle. The principle is simple: <strong>a component should only be a Client Component if it genuinely needs the client</strong> — state, effects, browser APIs, event handlers. Everything else can be a Server Component. A product description that just renders text? Server Component. A nav with static links? Server Component. A footer? Server Component. The question to ask for every component is: <em>does this need to run in the browser?</em></p><p>The vibe coding version asks: <em>does the tutorial mark this as </em><em>&#39;use client&#39;?</em> If yes, add it. If the app breaks, add more. This produces an RSC app that costs more than the original — in complexity, in security surface, in developer confusion — and delivers nothing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RjPfDlQ5Yvg43gdUbl_WTg.png" /></figure><h3>When You Don’t Need Server Components</h3><p>Dan Abramov and Joe Savona discussed the honest tradeoffs of RSC in their live stream with Kent C. Dodds. The team has never claimed RSC is the right tool for every app.</p><p>If your app is mostly interactive — drawing tools, real-time editors, complex forms — it consists almost entirely of Client Components anyway. RSC adds architectural complexity for minimal gain. If your API and frontend run in the same region, the round-trip savings from server-side fetching are negligible. The complexity isn’t justified by the performance return.</p><p>The more important consideration is team understanding. The React2Shell vulnerability was possible because the boundary between trusted server code and untrusted client input was blurry in the mental model. A team that treats Server Actions as internal functions will skip input validation — and that’s not a performance problem, it’s a critical security bug.</p><p>And if you’re adopting RSC because it’s what the new Next.js docs show, stop and ask what problem you’re solving. The React Labs blog post frames RSC as combining the simple request/response model of multi-page apps with the interactivity of single-page apps. That’s a genuine win for content-heavy apps with complex data requirements. For a CRUD app with a fast API and a small bundle, you may be adding architectural weight to solve a problem you don’t have.</p><p>Measure the actual loading problem first. Understand the tradeoff. Then decide.</p><h3>The One-Sentence Rule</h3><p>Use Server Components when you have a <strong>measured loading problem</strong> caused by JavaScript bundle size or data waterfalls — and your team treats every Server Action as a public HTTP endpoint that requires the same validation discipline as any API route. In every other case, measure the problem first.</p><h3>What’s Coming in Part 9</h3><p>Part 9 is the last article in the series. It answers the question Part 1 opened with: <em>“With vast differences in computing power and network speed, how do we deliver the best user experience for everyone?”</em> — pulling every piece of the series together into one complete picture.</p><h3>🎬 Watch These</h3><p><strong>Dan Abramov — </strong><a href="https://github.com/reactwg/server-components/discussions/5"><strong>RSC from Scratch (GitHub Discussion)</strong></a> The canonical technical deep dive — inventing RSC from first principles. The RSC payload format and the streaming architecture described in this article come directly from this document.</p><p><strong>Dan Abramov + Joe Savona — </strong><a href="https://www.youtube.com/watch?v=h7tur48JSaw"><strong>RSC Live Stream with Kent C. Dodds</strong></a> Honest discussion of RSC tradeoffs, limitations, and the mental model shift required. The source for the “when you don’t need RSC” framing.</p><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=V3Z_mJJjwPs&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=27"><strong>How basic hydration works internally in React?</strong></a> The tryToClaimNextHydratableInstance, fiber.stateNode matching, and cursor-based DOM walking described in this article.</p><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=7mSl7-OBfY8&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=30"><strong>What is Progressive Hydration and how does it work internally?</strong></a> Streaming HTML in chunks, Suspense comment node markers (&lt;!--$--&gt;, &lt;!--$!--&gt;), and the retry callback mechanism.</p><h3>🙏 Sources &amp; Thanks</h3><ul><li><strong>Dan Abramov</strong> — <a href="https://github.com/reactwg/server-components/discussions/5">RSC from Scratch (Part 1)</a> written by @gaearon on the reactwg GitHub. The PHP origin story, the RSC payload format, the streaming architecture, and the quote about JSX chunks all come directly from this document.</li><li><strong>The React Team</strong> — <a href="https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023">React Labs: March 2023</a> — the official Server Components definition quoted directly. <a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">Critical Security Vulnerability in React Server Components</a> — the official React2Shell disclosure, the vulnerability overview, and the timeline all come from the react.dev blog post.</li><li><strong>Lachlan Davidson</strong> — for discovering and responsibly reporting CVE-2025–55182 to Meta’s Bug Bounty program.</li><li><a href="https://jser.dev"><strong>jser.dev</strong></a> — the hydration mechanics (tryToClaimNextHydratableInstance, nextHydratableInstance cursor, fiber-to-DOM matching, selective hydration with Lanes priority, streaming with comment node markers) all come from JSer&#39;s source-level analysis: <a href="https://jser.dev/react/2023/03/17/how-does-hydration-work-in-react/">How basic hydration works</a>, <a href="https://jser.dev/react/2023/03/27/hydration-with-suspense/">How hydration works with Suspense</a>, <a href="https://jser.dev/react/2023/03/30/progressive-hydration/">What is Progressive Hydration</a>, and <a href="https://jser.dev/react/2023/04/10/guess-rsc/">My guess at how RSC works</a>.</li><li><strong>Unit 42 / Palo Alto Networks</strong> — for the post-exploitation analysis of CVE-2025–55182 documenting real-world attack patterns.</li><li><strong>Dan Abramov + Joe Savona</strong> — <a href="https://kentcdodds.com/blog/rsc-with-dan-abramov-and-joe-savona-live-stream">RSC Live Stream with Kent C. Dodds</a> — for the honest discussion of RSC tradeoffs that informed the “when you don’t need it” section.</li></ul><p><em>Part 9 is the final article — the full picture, from Part 1’s question to the complete answer. 🔧</em></p><p><strong>Tags:</strong> #react #javascript #webdev #tutorial #security</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=65f66b4d7bb1" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/how-react-works-part-8-server-components-hydration-the-real-story-65f66b4d7bb1">How React Works (Part 8)? Server Components &amp; Hydration: The Real Story</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How React Works (Part 7)? The Trap of Vibe Coding `useCallback`]]></title>
            <link>https://medium.com/codetodeploy/how-react-works-part-7-the-trap-of-vibe-coding-usecallback-e5548006b320?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/e5548006b320</guid>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react-usecallback]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Wed, 06 May 2026 08:29:39 GMT</pubDate>
            <atom:updated>2026-05-09T01:49:53.010Z</atom:updated>
            <content:encoded><![CDATA[<h3>The Trap of Vibe Coding useCallback</h3><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>Part 1:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-1-motivation-behind-react-fiber-time-slicing-suspense-062e6431d4b0"><em>Motivation Behind React Fiber: Time Slicing &amp; Suspense</em></a><em> </em><strong><em>Part 2:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-98e2f10ec0a7"><em>Why React Had to Build Its Own Execution Engine</em></a><em> </em><strong><em>Part 3:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-3-how-react-finds-what-actually-changed-35680df98712"><em>How React Finds What Actually Changed</em></a><em> </em><strong><em>Part 4:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97"><em>The Idea That Makes Suspense Possible</em></a><em> </em><strong><em>Part 5:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-25afdf54305b"><em>The React Lifecycle From the Inside</em></a><em> </em><strong><em>Part 6:</em></strong><em> </em><a href="https://medium.com/@samabaasi/how-react-works-part-6-how-state-actually-works-usestate-from-the-inside-51e2d785a420"><em>How State Actually Works</em></a><em> </em><strong><em>Prerequisites:</em></strong><em> Read Parts 1–6 first — especially Parts 3 and 6.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qdffSdTi2iZSw929YtZIEQ.png" /><figcaption><em>How React Works Under the Hood</em></figcaption></figure><blockquote><strong><em>🚀 Land Your Dream Tech Job in Just Weeks</em></strong></blockquote><blockquote><strong>💰 $50–$120/hr</strong> | Multiple Roles Open<br> Frontend • Backend • Full Stack • AI/ML • DevOps</blockquote><blockquote><a href="https://optimhire.com/?ref_code=codetodeploy">👉 <strong>Apply Now &amp; Get Hired Faster</strong></a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pkQFf0NUnzoFbVEVQCF5WQ.png" /></figure><h3>Where We Left Off</h3><p>In Part 6 we saw how useState works — state stored on the Fiber as a hook object, updates queued and processed on the next render, the component function running fresh from the top every time. Every render. From the top.</p><p>That last part is what this article is about. Because every render from the top means every value inside the component is recreated. And that single fact is the root cause of most React performance problems — and the reason the most common advice for fixing them often makes things worse.</p><h3>You Wrapped Everything in useCallback. You Added React.memo to Every Component. Your App Is Still Slow. What Went Wrong?</h3><p>This is the scene. You open the React DevTools Profiler. Components are re-rendering on every interaction. You’ve read the articles. You know the tools. So you do what everyone does:</p><pre>function SearchPage() {<br>  const [query, setQuery] = useState(&#39;&#39;);<br>  const [filters, setFilters] = useState({});<br><br>  const handleSearch = useCallback(() =&gt; search(query), [query]);<br>    const handleFilter = useCallback((key, val) =&gt; setFilters(f =&gt; ({...f, [key]: val})), []);<br>    const handleReset = useCallback(() =&gt; setFilters({}), []);<br>    const handleExport = useCallback(() =&gt; exportResults(query, filters), [query, filters]);<br>    const activeFilters = useMemo(<br>      () =&gt; Object.entries(filters).filter(([_, v]) =&gt; v),<br>      [filters]<br>    );<br>    return (<br>      &lt;ResultList<br>        onSearch={handleSearch}<br>        onFilter={handleFilter}<br>        onReset={handleReset}<br>        onExport={handleExport}<br>        activeFilters={activeFilters}<br>      /&gt;<br>    );<br>  }<br><br>  const ResultList = React.memo(({ onSearch, onFilter, onReset, onExport, activeFilters }) =&gt; {<br>    // ...<br>  });</pre><p>You deploy. You open the Profiler again. ResultList is still re-rendering. Some interactions are slower than before.</p><p>What went wrong?</p><p>This article answers that — from the inside out.</p><h3>React’s Default: Re-render Everything, Skip What It Can</h3><p>React’s mental model has always been simple: when state changes, re-render. Not just the component that changed — all of its children too.</p><p>But React has a built-in bailout system we covered in Part 3. When React walks down to a child, it checks one thing before running it:</p><blockquote><strong><em>Are the props the same object reference as last render?</em></strong></blockquote><p>If yes — same reference, not just same values — React skips that component entirely. The child doesn’t run. Its children don’t run. The entire subtree is skipped in one check. Free, automatic, no tools required.</p><p>The problem: in practice, props almost always get new references on every render — even when the values inside them are identical. And this is the root cause of most React performance problems.</p><h3>Every Render Creates New References Without You Noticing</h3><p>When a parent component re-renders, it runs its function from the top. Every line executes again:</p><pre>function SearchPage() {<br>  const [query, setQuery] = useState(&#39;&#39;);<br><br>  // ❌ New function object on every render<br>  const handleSearch = () =&gt; search(query);<br>  // ❌ New object on every render<br>  const style = { color: &#39;red&#39;, padding: &#39;8px&#39; };<br>  // ❌ New array on every render<br>  const results = data.filter(item =&gt; item.active);<br>  return &lt;ResultList onSearch={handleSearch} style={style} results={results} /&gt;;<br>}</pre><p>Every render, handleSearch is a brand new function. style is a brand new object. results is a brand new array. They look identical — same values, same contents — but they&#39;re different objects in memory.</p><p>React’s bailout uses === reference equality. Old handleSearch !== new handleSearch. So React can&#39;t bail out, and ResultList re-renders on every keystroke in the search input — even when the results haven&#39;t changed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NIalCc89HUkk9xSwZG3tsw.png" /></figure><h3>The Part Nobody Tells You: Children Re-render Even With No Props</h3><p>Here’s the insight that most performance guides miss entirely.</p><p>Given this structure:</p><pre>function SearchPage() {<br>  const [query, setQuery] = useState(&#39;&#39;);<br><br>return (<br>    &lt;div&gt;<br>      &lt;input value={query} onChange={e =&gt; setQuery(e.target.value)} /&gt;<br>      &lt;ResultList /&gt;  {/* no props at all */}<br>    &lt;/div&gt;<br>  );<br>}</pre><p>When the user types and SearchPage re-renders, ResultList re-renders too — even though it receives zero props.</p><p>Why? Because when SearchPage&#39;s function runs, it creates a <strong>new React element</strong> for &lt;ResultList /&gt;. That element object is new. When React&#39;s reconciler compares the old pendingProps to the new pendingProps on the ResultList fiber, they&#39;re different objects. So React re-renders ResultList. The issue isn&#39;t the prop values — it&#39;s where the element is created.</p><p>This means React.memo on ResultList alone doesn&#39;t help here at all.</p><h3>The Free Fix: Children as Props</h3><p>This is the most underused performance pattern in React. Zero memoization. Zero dependency arrays. Just a structural change.</p><pre>// ❌ Before — ResultList re-renders whenever SearchPage re-renders<br>function SearchPage() {<br>  const [query, setQuery] = useState(&#39;&#39;);<br>  return (<br>    &lt;div&gt;<br>      &lt;input value={query} onChange={e =&gt; setQuery(e.target.value)} /&gt;<br>      &lt;ResultList /&gt;<br>    &lt;/div&gt;<br>  );<br>}<br><br>// ✅ After - ResultList never re-renders when query changes<br>function App() {<br>  return (<br>    &lt;SearchPage&gt;<br>      &lt;ResultList /&gt;<br>    &lt;/SearchPage&gt;<br>  );<br>}<br>function SearchPage({ children }) {<br>  const [query, setQuery] = useState(&#39;&#39;);<br>  return (<br>    &lt;div&gt;<br>      &lt;input value={query} onChange={e =&gt; setQuery(e.target.value)} /&gt;<br>      {children}<br>    &lt;/div&gt;<br>  );<br>}<br><br>function SearchPage({ children }) {<br>  const [query, setQuery] = useState(&#39;&#39;);<br>  return (<br>    &lt;div&gt;<br>      &lt;input value={query} onChange={e =&gt; setQuery(e.target.value)} /&gt;<br>      {children}<br>    &lt;/div&gt;<br>  );<br>}</pre><p>Why does this work? The &lt;ResultList /&gt; element is now created inside App, not inside SearchPage. When SearchPage&#39;s state changes, App doesn&#39;t re-render. The children prop that SearchPage receives is the same object reference as before. React&#39;s built-in bailout kicks in — same reference, skip ResultList.</p><p>No React.memo. No useCallback. No overhead. Just structure.</p><p>The React docs state this directly: <em>“When a component visually wraps other components, let it accept JSX as children. This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render.”</em></p><p>Always try structural fixes before reaching for memoization tools. They’re free and they don’t break.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l1d01w5KyPShkwQChKFDQA.png" /></figure><h3>When You Actually Need React.memo</h3><p>Sometimes the structural fix isn’t possible. The child genuinely needs to be defined inside the parent and receive real props. That’s when React.memo enters.</p><p>React.memo wraps your component in a MemoComponent fiber. Instead of checking reference equality on the whole props object, React runs shallowEqual — comparing each individual prop value:</p><pre>const ResultList = React.memo(({ results, onSearch }) =&gt; {<br>  return &lt;div&gt;{results.map(r =&gt; &lt;Result key={r.id} data={r} /&gt;)}&lt;/div&gt;;<br>});</pre><p>If results and onSearch have the same values as last time, React bails out. Even if the props object itself is new.</p><p>One internal detail worth knowing: when you wrap a simple function component with no custom compare function and no defaultProps, React silently upgrades the fiber tag from MemoComponent to SimpleMemoComponent — a faster internal optimization path. Add a custom compare function and you lose this fast path.</p><p><strong>But </strong><strong>React.memo alone almost never works.</strong> If any prop is a function or object created inline during render, shallowEqual fails on that prop — new reference every time. React.memo bails out on nothing.</p><p>This is why in the opening example, ResultList still re-renders even though it&#39;s wrapped in React.memo. handleSearch, handleFilter, handleReset, handleExport — all created inline, all new references every render. React.memo sees four changed props and re-renders anyway.</p><h3>What useCallback and useMemo Actually Do — From the Source</h3><p>useCallback and useMemo exist for one reason: to stabilize references so React.memo can do its job.</p><p>Here’s the actual source code for both hooks — mountMemo on initial render and updateMemo on re-render:</p><pre>// From React source — initial render<br>function mountMemo(nextCreate, deps) {<br>  const hook = mountWorkInProgressHook(); // create hook in linked list<br>  const nextDeps = deps === undefined ? null : deps;<br>  const nextValue = nextCreate();          // run the factory fn<br>  hook.memoizedState = [nextValue, nextDeps]; // store [value, deps]<br>  return nextValue;<br>}<br><br>// From React source - every re-render<br>function updateMemo(nextCreate, deps) {<br>  const hook = updateWorkInProgressHook(); // walk to this hook in linked list<br>  const nextDeps = deps === undefined ? null : deps;<br>  const prevState = hook.memoizedState;   // [prevValue, prevDeps]<br>  if (prevState !== null &amp;&amp; nextDeps !== null) {<br>    const prevDeps = prevState[1];<br>    if (areHookInputsEqual(nextDeps, prevDeps)) {<br>      return prevState[0];               // deps unchanged → return cached value<br>    }<br>  }<br>  const nextValue = nextCreate();         // deps changed → recompute<br>  hook.memoizedState = [nextValue, nextDeps];<br>  return nextValue;<br>}</pre><p>updateCallback is identical — the only difference is it stores the function itself instead of calling it.</p><p>The dependency comparison runs through areHookInputsEqual:</p><pre>// From React source — runs on every render for every hook<br>function areHookInputsEqual(nextDeps, prevDeps) {<br>  for (let i = 0; i &lt; prevDeps.length &amp;&amp; i &lt; nextDeps.length; i++) {<br>    if (Object.is(nextDeps[i], prevDeps[i])) {<br>      continue;<br>    }<br>    return false; // one mismatch → recompute<br>  }<br>  return true;<br>}</pre><p>This loop runs on <strong>every render, for every hook, for every dependency</strong>. It doesn’t matter whether deps changed or not — the loop always runs.</p><h3>The Hidden Cost: What Happens on Every Render</h3><p>This is what the articles don’t tell you.</p><p>Even when dependencies haven’t changed and useCallback/useMemo return the cached value, React still does this on every render:</p><ol><li><strong>Walks to this hook in the linked list</strong> — updateWorkInProgressHook() traverses the hook chain every time</li><li><strong>Reads </strong><strong>hook.memoizedState</strong> — accesses [prevValue, prevDeps] from memory</li><li><strong>Runs </strong><strong>areHookInputsEqual</strong> — loops through every dependency with Object.is</li><li><strong>Allocates a new function</strong> (for useCallback) — React receives the new () =&gt; fn you wrote, then decides whether to discard it</li></ol><p>That last point surprises most developers. When you write:</p><pre>const handleSearch = useCallback(() =&gt; search(query), [query]);</pre><p>JavaScript creates the arrow function () =&gt; search(query) <strong>before</strong> calling useCallback. React receives it, runs areHookInputsEqual, and if deps haven&#39;t changed, discards the new function and returns the old one. The allocation happened regardless.</p><p>So useCallback(fn, [a, b, c]) with unchanged deps on every render means: one function allocation (discarded), three Object.is comparisons, one linked list traversal. For a component that re-renders 50 times per second during a scroll, that&#39;s 150 comparisons per second — for a component that might have been fast to render in 0.1ms anyway.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WeFANbZArstN-3lKhkkWPg.png" /></figure><h3>The Vibe Coding Trap</h3><p>Here’s what actually happens in most codebases.</p><p>Someone reads that useCallback prevents re-renders. They add it to every function. They add React.memo to every component. They add useMemo to every derived value. &quot;Just to be safe.&quot; Copy-paste from the last project. Vibe coding.</p><pre>// Real pattern from real codebases<br>function ProfileCard({ userId }) {<br>  const handleMouseEnter = useCallback(() =&gt; setHovered(true), []);<br>  const handleMouseLeave = useCallback(() =&gt; setHovered(false), []);<br>  const handleFocus = useCallback(() =&gt; setFocused(true), []);<br>  const handleBlur = useCallback(() =&gt; setFocused(false), []);<br>  const containerClass = useMemo(<br>    () =&gt; `card ${hovered ? &#39;card--hovered&#39; : &#39;&#39;} ${focused ? &#39;card--focused&#39; : &#39;&#39;}`,<br>    [hovered, focused]<br>  );<br><br>return (<br>    &lt;div<br>      className={containerClass}<br>      onMouseEnter={handleMouseEnter}<br>      onMouseLeave={handleMouseLeave}<br>      onFocus={handleFocus}<br>      onBlur={handleBlur}<br>    &gt;<br>      &lt;UserAvatar userId={userId} /&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><p>This is a card component. It renders in under 0.1ms. Nothing inside it is expensive. UserAvatar doesn&#39;t receive any of the memoized values. The containerClass string is trivially fast to compute.</p><p>Every single useCallback and useMemo here adds overhead — dependency comparisons, hook traversals, function allocations — for zero performance benefit. The component was never the bottleneck.</p><p>And the real cost shows up in debugging. When containerClass produces the wrong value, you now have to trace through useMemo and its [hovered, focused] deps to understand why. When handleFocus behaves unexpectedly, you check the useCallback closure. What was a simple state bug becomes a memoization debugging session.</p><p>The React docs are direct about this: <em>“There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside of this approach is that code becomes less readable. Also, not all memoization is effective: a single value that’s ‘always new’ is enough to break memoization for an entire component.”</em></p><h3>The Right Order</h3><p>Here’s the order that actually fixes performance problems:</p><p><strong>1. Measure first.</strong> Open React DevTools Profiler. Record a session. Find the component that’s actually slow — its render time, how often it renders, and why. Don’t optimize what you haven’t measured. Most of the time the bottleneck is not where you think it is.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LBvwO0iI5lupfWSv12cDdg.png" /></figure><p><strong>2. Fix the structure.</strong> Can the slow child’s element be created in a parent that doesn’t re-render? Can state be moved down closer to where it’s used? Can you use the children-as-props pattern? These fixes are free — no memoization overhead, no stale closure risk, no dependency arrays to maintain.</p><p><strong>3. Memoize if structure can’t fix it.</strong> If structural fixes aren’t possible and the component is genuinely expensive to render and receives props that could be stable — then reach for React.memo + useCallback/useMemo together. All three are needed. React.memo without stable references does nothing. useCallback without React.memo on the child does nothing.</p><p><strong>4. Verify it worked.</strong> Re-run the Profiler. Confirm the component no longer re-renders unnecessarily. If it still does, find what reference is still unstable and trace it back.</p><p>The rule, in one sentence: <strong>don’t add memoization until you have a specific, measured component that re-renders too often, with expensive renders, and props that can be stabilized.</strong></p><h3>A Note on the React Compiler</h3><p>The React Compiler (previously React Forget) is now stable. It’s a build-time Babel plugin that automatically applies memoization where it’s safe — analyzing your components, inferring dependencies, and inserting optimizations without you writing any of it.</p><p>But it only works on components that follow the Rules of React: pure renders, no prop mutation, no non-deterministic values during render. Code that follows these rules gets automatic optimization. Code that doesn’t gets de-opted — the Compiler falls back and does nothing.</p><p>Here’s the important point: understanding everything in this article — why references matter, why structural fixes come first, why useCallback has a cost — is exactly what you need to write code the Compiler can optimize. The Compiler doesn&#39;t replace understanding React&#39;s rendering model. It rewards you for having it.</p><h3>What’s Coming in Part 8</h3><p>In Part 8 we go into Server Components and Hydration — how React renders on the server, streams the result to the client, and hands off interactivity without re-doing all the work. The rendering model we’ve built throughout this series extends to the server in some surprising ways.</p><h3>🎬 Watch These</h3><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=LfwMlGjiaW0&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=13"><strong>How does React bailout work in reconciliation?</strong></a> The source for the “children re-render even with no props” insight and the children-as-props bailout pattern. The demo shows exactly which components re-render and why.</p><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=0jbV6apamhs&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=14"><strong>How does React.memo() work internally?</strong></a> The MemoComponent vs SimpleMemoComponent internal distinction and shallowEqual — the source for the React.memo internals section.</p><h3>🙏 Sources &amp; Thanks</h3><ul><li><a href="https://jser.dev/"><strong>jser.dev</strong></a> — the “children re-render even with no props” insight and the children-as-props bailout technique come directly from <a href="https://jser.dev/react/2022/01/07/how-does-bailout-work/">How does React bailout work?</a>. The MemoComponent/SimpleMemoComponent distinction from <a href="https://jser.dev/react/2022/01/11/how-react-memo-works/">How does React.memo() work internally?</a></li><li><strong>React source</strong> — mountMemo, updateMemo, mountCallback, updateCallback, and areHookInputsEqual are taken directly from packages/react-reconciler/src/ReactFiberHooks.js in the <a href="https://github.com/facebook/react">facebook/react</a> repo. Verified via multiple source walkthroughs.</li><li><strong>React docs</strong> — the children-as-props principle and the “not all memoization is effective” quote come directly from <a href="https://react.dev/reference/react/memo">react.dev/reference/react/memo</a>. The React Compiler section draws from <a href="https://react.dev/learn/react-compiler/introduction">react.dev/learn/react-compiler</a>.</li><li><a href="https://www.lydiahallie.io/"><strong>Lydia Hallie</strong></a> — for JavaScript visualizations that shaped this series’ style.</li></ul><p><em>Part 8 is next — Server Components and Hydration: how React moved rendering to the server without breaking the client model. 🔧</em></p><p><strong>Tags:</strong> #react #javascript #webdev #performance #tutorial</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9QTaaaxboQP_gKSLedW_w.png" /></figure><p>👉 Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></p><p>👉 Follow us: <a href="https://www.linkedin.com/in/bhumika-ch-3784391b9/"><strong>Linkedin</strong></a>| <a href="https://medium.com/codetodeploy"><strong>Medium</strong></a></p><p>👉 CodeToDeploy Tech Community is live on Discord — <a href="https://discord.gg/ZpwhHq6D"><strong>Join now!</strong></a></p><p><strong>Disclosure:</strong> This post includes affiliate and partnership links.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e5548006b320" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codetodeploy/how-react-works-part-7-the-trap-of-vibe-coding-usecallback-e5548006b320">How React Works (Part 7)? The Trap of Vibe Coding `useCallback`</a> was originally published in <a href="https://medium.com/codetodeploy">CodeToDeploy</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How React Works (Part 6)? How State Actually Works: useState from the Inside]]></title>
            <link>https://javascript.plainenglish.io/how-react-works-part-6-how-state-actually-works-usestate-from-the-inside-51e2d785a420?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/51e2d785a420</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[react-hook]]></category>
            <category><![CDATA[hooks]]></category>
            <category><![CDATA[react-hook-usestate]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Tue, 05 May 2026 11:17:00 GMT</pubDate>
            <atom:updated>2026-05-06T06:50:10.044Z</atom:updated>
            <content:encoded><![CDATA[<h3>How State Actually Works: useState from the Inside</h3><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>Part 1:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-1-motivation-behind-react-fiber-time-slicing-suspense-062e6431d4b0"><em>Motivation Behind React Fiber: Time Slicing &amp; Suspense</em></a><em> </em><strong><em>Part 2:</em></strong><em> </em><a href="https://blog.stackademic.com/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-98e2f10ec0a7"><em>Why React Had to Build Its Own Execution Engine</em></a><em> </em><strong><em>Part 3:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-3-how-react-finds-what-actually-changed-35680df98712"><em>How React Finds What Actually Changed</em></a><em> </em><strong><em>Part 4:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97"><em>The Idea That Makes Suspense Possible</em></a><em> </em><strong><em>Part 5:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-25afdf54305b"><em>The React Lifecycle From the Inside</em></a><em> </em><strong><em>Prerequisites:</em></strong><em> Read Parts 1–5 first.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qdffSdTi2iZSw929YtZIEQ.png" /><figcaption><em>How React Works Under the Hood</em></figcaption></figure><h3>Three Things That Confuse Almost Everyone</h3><p>Here are three behaviors of useState that trip up developers at every level:</p><pre>function Counter() {<br>  const [count, setCount] = useState(0);<br><br>  function handleClick() {<br>      setCount(count + 1);<br>      setCount(count + 1);<br>      setCount(count + 1);<br>      // You might expect count to become 3<br>      // It becomes 1<br>    }<br><br>    function handleLog() {<br>      setCount(count + 1);<br>      console.log(count);<br>      // You might expect to see the new value<br>      // You see the old value<br>    }<br><br>    function handleSame() {<br>      setCount(count); // setting same value<br>      // You might expect no re-render<br>      // Sometimes React still re-renders once<br>    }<br>}</pre><p>All three behaviors make complete sense once you understand how useState actually works under the hood. This article explains all three — and by the end, you&#39;ll never be surprised by state again.</p><h3>Where State Lives</h3><p>The most important question to start with: when you call useState(0), where does that 0 actually live?</p><p>Not in the component function. Component functions are just regular JavaScript functions — they have no persistent memory between calls. Every time React re-renders your component, it calls the function fresh from the top.</p><p>State lives on the <strong>Fiber</strong>. Specifically, on a hook object stored in the fiber’s memoizedState field.</p><p>Every hook call creates a new hook object and appends it to a linked list hanging off the fiber:</p><pre>Counter fiber<br>  └─ memoizedState → hook (useState count)<br>                       └─ next → hook (useEffect)<br>                                   └─ next → hook (useRef)<br>                                               └─ next → null</pre><p>Each hook object stores the current state value in its own memoizedState field, plus an updateQueue for pending updates, plus a link to the next hook.</p><p>This is why the rules of hooks exist — specifically why you can’t call hooks conditionally. React doesn’t track hooks by name. It tracks them by position in the linked list. Call number 1 is always useState count, call number 2 is always useEffect, and so on. If you skip a hook call with an if statement, every subsequent hook gets the wrong state from the wrong position.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DjJugAAuwGyWkQfGWmS7cA.png" /></figure><h3>What Happens When You Call setState</h3><p>Here’s what most developers picture when they call setCount(count + 1):</p><blockquote><em>React updates the count, then re-renders the component.</em></blockquote><p>Here’s what actually happens:</p><blockquote><em>React creates a small update object and adds it to a queue. Then it schedules a re-render for later. The component function does not run again right now.</em></blockquote><p>That’s it. Calling setState is not synchronous. It&#39;s not instant. It&#39;s closer to leaving a note — &quot;please re-render this component with this new value when you get a chance.&quot;</p><p>Here is the actual sequence:</p><p><strong>Step 1 — Create an update object.</strong> React packages your new value (or updater function) into a small object that also carries the current priority lane.</p><p><strong>Step 2 — Stash it in a queue.</strong> The update goes into a global buffer (concurrentQueues). It&#39;s not attached to the fiber yet — React is potentially in the middle of rendering something else and can&#39;t safely modify the fiber tree right now.</p><p><strong>Step 3 — Schedule a re-render.</strong> React calls scheduleUpdateOnFiber, which walks up to the root, marks the trail of childLanes, and registers a task with the Scheduler (from Part 2). The Scheduler will call back to run the render.</p><p><strong>Step 4 — At the start of the next render, attach updates.</strong> Before the render begins, finishQueueingConcurrentUpdates runs and attaches all stashed updates from the global buffer to their respective fibers&#39; queues. Now the fiber is ready to process them.</p><p><strong>Step 5 — During render, process the queue.</strong> When React processes the Counter fiber, it reads hook.queue, processes the pending updates in order, and the result becomes the new hook.memoizedState — the new value that useState will return for this render.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l-2NLczealA1wlf6-ZmGNw.png" /></figure><h3>A Concrete Trace: One Button Click</h3><p>Let’s make this real. The Counter from the opening — count starts at 0, user clicks the button:</p><p><strong>The click happens.</strong> The browser fires a click event. React’s synthetic event handler calls your handleClick, which calls setCount(count + 1) — that&#39;s setCount(1).</p><p><strong>dispatchSetState runs.</strong> React creates an update object { action: 1, lane: SyncLane } and pushes it into the global concurrentQueues buffer. The Counter fiber is unchanged — it still has hook.memoizedState = 0. The component function has not run again yet.</p><p><strong>scheduleUpdateOnFiber runs.</strong> React walks up from the Counter fiber to the root, marking childLanes on every ancestor. The Scheduler registers a task to call performConcurrentWorkOnRoot.</p><p><strong>The event handler finishes.</strong> React checks for other queued updates — there are none. Since this was a user interaction (SyncLane), React runs the render synchronously rather than waiting for the next macro task.</p><p><strong>finishQueueingConcurrentUpdates runs.</strong> The pending update is moved from the global buffer and attached to hook.queue.pending on the Counter fiber. The fiber is now ready.</p><p><strong>React renders the Counter fiber.</strong> updateState reads hook.queue, finds the pending update { action: 1 }, runs it through basicStateReducer, and gets 1. This becomes the new hook.memoizedState. The component function runs with count = 1 and returns new JSX.</p><p><strong>Commit phase.</strong> React finds the changed text node and updates button.textContent to &quot;click 1&quot;. The DOM reflects the new state.</p><p>Total time from click to DOM update: a few milliseconds. Total re-renders: exactly one.</p><h3>Why You See the Old Value After setState</h3><p>Now the first confusion makes sense.</p><pre>setCount(count + 1);<br>console.log(count); // still the old value</pre><p>Calling setCount didn&#39;t change count. It created an update object and scheduled a re-render. The variable count is a local variable in the current function call — it was captured when the component rendered and it will never change during this render. The new value only exists after the next render completes and useState reads from the processed queue.</p><p>This is a fundamental property of React’s model: <strong>state values are snapshots.</strong> Every render captures its own version of state, and that snapshot is frozen for the duration of that render.</p><h3>Why Three setCount Calls Only Increment Once</h3><pre>setCount(count + 1);<br>setCount(count + 1);<br>setCount(count + 1);<br>// count goes from 0 to 1, not 0 to 3</pre><p>Each of these three calls captures the same count — the snapshot from the current render, which is 0. So all three are equivalent to setCount(0 + 1). React queues three updates, all with value 1. When it processes them in the next render, the result is 1.</p><p>The fix is the updater function form:</p><pre>setCount(c =&gt; c + 1);<br>setCount(c =&gt; c + 1);<br>setCount(c =&gt; c + 1);<br>// count goes from 0 to 3</pre><p>When you pass a function, React processes the updates in sequence — each one receives the result of the previous. The queue runs 0 → 1 → 2 → 3. The updater function form is specifically designed for when you need to chain updates.</p><h3>Why Multiple setState Calls Don&#39;t Cause Multiple Re-renders</h3><p>This is <strong>batching</strong> — and it’s one of React’s most important performance features.</p><p>When you call setState multiple times in the same event handler, React doesn&#39;t re-render after each call. It collects all the updates, then does a single re-render at the end.</p><pre>function handleClick() {<br>  setCount(c =&gt; c + 1);  // queued<br>  setName(&#39;Alice&#39;);       // queued<br>  setLoading(false);      // queued<br>  // → one re-render, not three<br>}</pre><p>Updates are stashed in the global concurrentQueues buffer and only attached to fibers at the beginning of the next render via finishQueueingConcurrentUpdates. This means all three setState calls during the same event handler land in the queue before any render begins — React processes them all in one pass.</p><p>Before React 18, batching only happened inside React event handlers. In setTimeout, fetch callbacks, or native event handlers, each setState would trigger a separate re-render. React 18 introduced <strong>automatic batching</strong> — batching now happens everywhere by default, regardless of where the update originates.</p><h3>The Same-Value Optimization (and Its Catch)</h3><p>What happens when you call setState with the same value the state already has?</p><pre>setCount(count); // count is already 5, setting it to 5 again</pre><p>React tries to skip the re-render entirely. Before scheduling the re-render, dispatchSetState eagerly computes the new state and compares it to the current state using Object.is. If they&#39;re identical, React calls enqueueConcurrentHookUpdateAndEagerlyBailout and returns immediately — no render is scheduled.</p><p>But here’s the catch: this bailout only happens when the fiber’s update queue is currently empty. If there’s other pending work on the component, React can’t safely apply this optimization and may still re-render once. The comment in the React source code says “React tries to avoid scheduling re-render with best effort, but no guarantee.”</p><p>So the rule in practice: setting state to the same value usually prevents a re-render, but not always. Don’t write code that depends on this optimization for correctness.</p><h3>useRef: State Without Re-renders</h3><p>Now that you understand how useState works, useRef becomes obvious.</p><p>useRef is implemented almost identically to useState — it creates a hook object on the fiber&#39;s linked list, stores its value in memoizedState. The difference: updating a ref doesn&#39;t create an update object and doesn&#39;t call scheduleUpdateOnFiber. It just mutates the current property of the ref object directly.</p><pre>const ref = useRef(0);<br>ref.current = 1; // direct mutation — no queue, no schedule, no re-render</pre><p>Because React never learns about the change, it never re-renders. The new value is available immediately (no snapshot problem), but React won’t react to it. This makes useRef perfect for values that need to persist across renders without triggering them — timer IDs, previous values, DOM node references.</p><p>The tradeoff is exactly what you’d expect: refs are live, mutable, immediate — but invisible to React’s rendering system.</p><h3>The Full Picture</h3><p>useState is not a magical React primitive. It&#39;s a hook object on a linked list on a fiber, with a queue for pending updates and a scheduler that processes them. Every behavior that seems surprising becomes logical once you see the structure:</p><p>The snapshot problem exists because state is stored on the fiber, not in a mutable variable — and the fiber gets its new value only after the render processes the queue. The batching behavior exists because updates go into a global buffer before being attached to fibers. The same-value optimization exists because React checks eagerly before scheduling. And useRef is simply the same structure without the scheduling step.</p><p>State in React is not instant, synchronous, or mutable in the traditional sense. It’s a description of what the next render should look like — and React processes that description on its own schedule, in its own order, using the Fiber and Scheduler machinery from Parts 2 and 3.</p><h3>What’s Coming in Part 7</h3><p>In Part 7 we look at performance — not the solutions first, but the problems. What actually causes unnecessary re-renders? Why does passing a new object as a prop matter? When is React doing work it doesn’t need to? Understanding the problems clearly is what makes React.memo, useMemo, and useCallback make sense — rather than things you add until the lag goes away.</p><h3>🎬 Watch These</h3><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=svaUEHMuv9w&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=5"><strong>How does useState() work internally in React?</strong></a> The primary source for this entire article — mountState, dispatchSetState, concurrentQueues, finishQueueingConcurrentUpdates, the eager bailout, and the batching mechanism. All sourced from here.</p><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=0GM-1W7i9Tk&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=3"><strong>How does React re-render internally?</strong></a> How the update queue is processed during the render phase — the other half of the state story.</p><h3>🙏 Sources &amp; Thanks</h3><ul><li><a href="https://jser.dev/"><strong>jser.dev</strong></a> — every mechanism in this article comes from JSer’s source-level analysis of useState. The mountState flow, dispatchSetState internals, concurrentQueues global buffer, finishQueueingConcurrentUpdates, the eager same-value bailout with the &quot;best effort, no guarantee&quot; caveat, and the batching explanation all come directly from <a href="https://jser.dev/2023-06-19-how-does-usestate-work/">How does useState() work internally in React?</a></li><li><strong>React source</strong> — packages/react-reconciler/src/ReactFiberHooks.js (mountState, dispatchSetState, mountWorkInProgressHook) and packages/react-reconciler/src/ReactFiberConcurrentUpdates.js (enqueueConcurrentHookUpdate, finishQueueingConcurrentUpdates) in the <a href="https://github.com/facebook/react">facebook/react</a> repo.</li><li><a href="https://www.lydiahallie.io/"><strong>Lydia Hallie</strong></a> — for JavaScript visualizations that shaped this series’ style.</li></ul><p><em>Part 7 is next — performance: what actually causes unnecessary re-renders, before we reach for any optimization tools. 🔧</em></p><p><strong>Tags:</strong> #react #javascript #webdev #tutorial</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=51e2d785a420" width="1" height="1" alt=""><hr><p><a href="https://javascript.plainenglish.io/how-react-works-part-6-how-state-actually-works-usestate-from-the-inside-51e2d785a420">How React Works (Part 6)? How State Actually Works: useState from the Inside</a> was originally published in <a href="https://javascript.plainenglish.io">JavaScript in Plain English</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How React Works (Part 5)? The React Lifecycle From the Inside: When Things Actually Run]]></title>
            <link>https://blog.stackademic.com/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-25afdf54305b?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/25afdf54305b</guid>
            <category><![CDATA[lifecycle]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[react-hook]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Tue, 05 May 2026 07:29:13 GMT</pubDate>
            <atom:updated>2026-05-06T07:03:15.882Z</atom:updated>
            <content:encoded><![CDATA[<h3>The React Lifecycle From the Inside: When Things Actually Run</h3><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>Part 1:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-1-motivation-behind-react-fiber-time-slicing-suspense-062e6431d4b0"><em>Motivation Behind React Fiber: Time Slicing &amp; Suspense</em></a><em> </em><strong><em>Part 2:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-98e2f10ec0a7"><em>Why React Had to Build Its Own Execution Engine</em></a><em> </em><strong><em>Part 3:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-3-how-react-finds-what-actually-changed-35680df98712?postPublishedType=repub"><em>How React Finds What Actually Changed</em></a><em> </em><strong><em>Part 4:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97?postPublishedType=repub"><em>The Idea That Makes Suspense Possible</em></a><em> </em><strong><em>Prerequisites:</em></strong><em> Read Parts 1–4 first.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qdffSdTi2iZSw929YtZIEQ.png" /><figcaption><em>How React Works Under the Hood</em></figcaption></figure><h3>A Puzzle Most React Developers Get Wrong</h3><p>Quick quiz. What order do these log?</p><pre>function App() {<br>  useLayoutEffect(() =&gt; {<br>    console.log(&#39;layout effect&#39;);<br>  });<br><br>  useEffect(() =&gt; {<br>    console.log(&#39;effect&#39;);<br>  });<br><br>  console.log(&#39;render&#39;);<br>  return &lt;div /&gt;;<br>}</pre><p>Most people guess: render → effect → layout effect, or render → layout effect → effect.</p><p>The answer is: <strong>render → layout effect → effect.</strong></p><p>But more importantly — <em>why?</em> Why does useLayoutEffect run before useEffect? Why does useEffect run at all after the component already returned? What does &quot;after the browser paints&quot; actually mean, and is that even always true?</p><p>This article answers all of that. And by the end, you’ll be able to look at any component and predict exactly when each piece of it runs — and why.</p><h3>The Three Phases of a React Render</h3><p>Before we get into effects, we need to remember the pipeline from Part 2. Every React update goes through three phases:</p><p><strong>Render</strong> — React runs your component functions, diffs the trees, figures out what changed. Nothing is written to the DOM yet. This is where console.log(&#39;render&#39;) fires.</p><p><strong>Commit</strong> — React takes everything it figured out during Render and applies it to the real DOM. This is synchronous and uninterruptible.</p><p><strong>Effects</strong> — After the DOM is updated, React runs your effects.</p><p>The key insight is that effects are <em>not</em> part of rendering. They’re a separate step that happens after the DOM is already updated. This is why you can safely read the DOM inside an effect — by the time it runs, the DOM reflects the current state.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rxweigdJKGRKS47xQe8inw.png" /></figure><h3>What useEffect Actually Is</h3><p>Here’s the mental model most developers have: useEffect is &quot;code that runs after render.&quot; That&#39;s roughly right but missing the important details.</p><p>Here’s the more accurate model: <strong>useEffect is a declaration that you have side effects to run after React is done with the DOM.</strong> You&#39;re not scheduling a callback — you&#39;re telling React about work that needs to happen, and React decides when to run it.</p><p>The distinction matters because React doesn’t run effects immediately after commit. It <em>schedules</em> them.</p><p>After the Commit phase finishes updating the DOM, React schedules your useEffect callbacks as a separate task in the Scheduler&#39;s queue — the same Scheduler we covered in Part 2. That means effects run in a <strong>new macro task</strong>, after the browser has had a chance to paint the updated screen.</p><p>This is why the React docs say useEffect runs &quot;after the browser paints.&quot; The sequence looks like this:</p><pre>1. Render phase — your component functions run<br>2. Commit phase — DOM is updated<br>3. Browser paints the screen<br>4. Scheduler fires — useEffect callbacks run</pre><p>The browser gets step 3 because steps 1–2 are one macro task, and step 4 is a new macro task. Between any two macro tasks, the browser can paint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1aDGkfxftjFWfNhyecVubA.png" /></figure><h3>The Cleanup: Why It Runs Before the Next Effect</h3><p>Every useEffect can return a cleanup function:</p><pre>useEffect(() =&gt; {<br>  const subscription = subscribe(id);<br>  return () =&gt; subscription.unsubscribe(); // cleanup<br>}, [id]);</pre><p>When id changes and the effect needs to re-run, React runs the <em>cleanup of the previous effect first</em>, then runs the new effect. Always in that order.</p><p>This is also true on unmount — the cleanup runs when the component leaves the tree.</p><p>The reason is straightforward: React never wants two instances of the same effect active at the same time. Before setting up the new subscription, it tears down the old one. This is React being deliberate about side effects — always clean up before you set up again.</p><p>Cleanups run first in commitPassiveUnmountEffects, then new effects run in commitPassiveMountEffects. Both happen in the same scheduled task, in the same order as the tree (children before parents, same as completeWork).</p><h3>useLayoutEffect: The Synchronous Version</h3><p>Here’s the critical difference: useLayoutEffect runs <strong>synchronously inside the Commit phase</strong>, before the browser paints.</p><p>The sequence with useLayoutEffect:</p><pre>1. Render phase — component functions run<br>2. Commit phase — DOM is updated<br>3. useLayoutEffect callbacks run  ← here, synchronously, before paint<br>4. Browser paints<br>5. useEffect callbacks run        ← here, in next macro task</pre><p>This is why useLayoutEffect fires before useEffect in our opening quiz — it&#39;s not &quot;earlier in the same phase,&quot; it&#39;s in a completely different phase.</p><p>And this is why useLayoutEffect exists at all. If you need to <strong>read the DOM after it&#39;s updated but before the user sees it</strong> — for example, measuring an element&#39;s size to position a tooltip — useLayoutEffect is the only place where that&#39;s safe and accurate.</p><pre>// useLayoutEffect — correct for DOM measurements<br>useLayoutEffect(() =&gt; {<br>  const rect = ref.current.getBoundingClientRect();<br>  setTooltipPosition({ top: rect.bottom, left: rect.left });<br>});<br><br><br>// useEffect - would cause a visible flicker for measurements<br>// because the browser already painted before this runs<br>useEffect(() =&gt; {<br>  const rect = ref.current.getBoundingClientRect();<br>  setTooltipPosition({ top: rect.bottom, left: rect.left }); // too late<br>});</pre><p>If you use useEffect for a DOM measurement that affects layout, the user will briefly see the wrong layout (before useEffect runs), then see it jump to the correct layout (after). That flicker is exactly what useLayoutEffect prevents.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PD4B-0Hn8lepZo4z5v6JQQ.png" /></figure><h3>The “After Paint” Rule Has an Exception</h3><p>Here’s something that React’s own documentation gets wrong.</p><p>The docs say useEffect always runs after the browser paints. But that&#39;s not strictly true.</p><p>Sometimes React runs useEffect callbacks <em>before</em> paint. This happens when React determines it&#39;s more important to show the latest UI as quickly as possible — for example, when a re-render is triggered by a user interaction, or when effects are scheduled under layout effects. In those cases, React won&#39;t wait for a paint before running the effects.</p><p>From jser.dev’s paint timing analysis:</p><blockquote>“I’d explain the timing of running useEffect() callbacks as follows. useEffect() callbacks are run after DOM mutation is done. Most of the time, they are run asynchronously after paint, but React might run them synchronously before paint when it is more important to show the latest UI — for example when re-render is caused by user interactions or scheduled under layout effects, or simply if React has the time internally to do so.”</blockquote><p>The practical implication: never rely on useEffect to run after paint for correctness. If your code requires running after the browser paints, useLayoutEffect with its explicit synchronous-before-paint guarantee is actually the more predictable choice. And if you truly need to run something after paint for non-DOM reasons, the gap is usually small enough not to matter.</p><h3>The Dependency Array: What It Actually Controls</h3><p>The dependency array in useEffect and useLayoutEffect doesn&#39;t control <em>whether</em> the effect runs — it controls <em>when</em> it re-runs.</p><p>React compares the current values of the dependency array to the previous ones using Object.is (similar to === but handles NaN and -0 correctly). If any value changed, React marks the effect with a flag indicating it should run again. If nothing changed, the effect is skipped this cycle.</p><pre>useEffect(() =&gt; {<br>  fetchUser(id);<br>}, [id]); // only re-runs when id changes</pre><p>Three cases:</p><p><strong>No dependency array</strong> — re-runs after every render. The effect has no conditions.</p><p><strong>Empty array </strong><strong>[]</strong> — runs once on mount, cleanup runs on unmount. Effect has no dependencies that can change.</p><p><strong>Array with values</strong> — re-runs whenever any listed value changes between renders.</p><p>The important thing to understand: React doesn’t track <em>what</em> you use inside the effect. It trusts the dependency array you provide. If you use userId inside the effect but forget to put it in the array, React won&#39;t re-run the effect when userId changes — and you get a stale value bug. This is why eslint-plugin-react-hooks exists: it statically analyzes your effect and warns when the array is incomplete.</p><h3>The Full Lifecycle in One Timeline</h3><p>The pattern is consistent across every scenario. On mount, the component renders, React commits the DOM, useLayoutEffect fires synchronously before the browser paints, the browser paints, then useEffect runs in the next macro task. On update, cleanups always run before new effects — layout cleanup first, then layout create, then paint, then passive cleanup, then passive create. On unmount, both cleanups run in the same order, layout before passive, with paint in between.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZY5T_0Yc2BW3KxfFj7ntUg.png" /></figure><p>Two things are always true regardless of the scenario: layout effects are synchronous and pre-paint, passive effects are scheduled and post-commit. And cleanups always run before the next effect of the same type — React never has two instances of the same effect active at once.</p><h3>When to Use Which</h3><p>Use useEffect for everything by default. Most side effects — data fetching, subscriptions, logging, timers — don&#39;t need to happen before the browser paints. Running them after paint keeps the UI responsive and is the right choice in the vast majority of cases.</p><p>Reach for useLayoutEffect only when you need to read or modify the DOM before the user sees it — measuring element dimensions, positioning a tooltip relative to another element, or preventing a visual flicker. The practical test is simple: if swapping useLayoutEffect for useEffect causes a visible jump or flash in the UI, you needed useLayoutEffect. If it looks identical, stick with useEffect.</p><h3>What’s Coming in Part 6</h3><p>In Part 6 we go inside useState — how state is stored on the Fiber, what happens when you call setState, and why calling setState multiple times in one event handler doesn&#39;t cause multiple re-renders.</p><h3>🎬 Watch These</h3><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=Ggmdo7TORNc&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=16"><strong>The lifecycle of effect hooks in React</strong></a> The source for the Effect object internals, HookHasEffect tag, flushPassiveEffects, and cleanup ordering in this article.</p><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=6HLvyiYv7HI&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=10"><strong>How does useLayoutEffect() work internally?</strong></a> The synchronous commit-phase timing of layout effects vs passive effects — the source for the timing difference explained here.</p><p><strong>JSer (jser.dev) — </strong><a href="https://www.youtube.com/watch?v=2iXKUJpnALU&amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;index=35"><strong>When do useEffect() callbacks get run? Before paint or after paint?</strong></a> The surprising finding that React.dev’s “always after paint” description is inaccurate — and the real timing rules.</p><h3>🙏 Sources &amp; Thanks</h3><ul><li><a href="https://jser.dev/"><strong>jser.dev</strong></a> — all timing mechanics in this article come from JSer’s source-level analysis. Articles used: <a href="https://jser.dev/react/2022/01/19/lifecycle-of-effect-hook/">The lifecycle of effect hooks in React</a>, <a href="https://jser.dev/react/2021/12/04/how-does-useLayoutEffect-work/">How does useLayoutEffect() work internally?</a>, <a href="https://jser.dev/2023-07-08-how-does-useeffect-work/">How does useEffect() work internally in React?</a>, and <a href="https://jser.dev/2023-08-09-effects-run-paint/">When do useEffect() callbacks get run?</a> — including the quote about React.dev’s inaccurate description.</li><li><strong>React source</strong> — ReactFiberCommitWork.js (commitLayoutEffects, commitPassiveMountEffects, commitPassiveUnmountEffects) and ReactFiberWorkLoop.js (scheduleCallback for passive effects).</li><li><a href="https://www.lydiahallie.io/"><strong>Lydia Hallie</strong></a> — for JavaScript visualizations that shaped this series’ style.</li></ul><p><em>Part 6 is next — how </em><em>useState actually works: where state lives, what happens when you call </em><em>setState, and why multiple calls in one handler don&#39;t cause multiple re-renders. 🔧</em></p><p><strong>Tags:</strong> #react #javascript #webdev #tutorial</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25afdf54305b" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-25afdf54305b">How React Works (Part 5)? The React Lifecycle From the Inside: When Things Actually Run</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How React Works (Part 4)? The Idea That Makes Suspense Possible]]></title>
            <link>https://blog.stackademic.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb96d5a7ad97</guid>
            <category><![CDATA[suspense]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[react-fiber-architecture]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Mon, 04 May 2026 10:25:24 GMT</pubDate>
            <atom:updated>2026-05-06T07:03:10.832Z</atom:updated>
            <content:encoded><![CDATA[<h3>The Idea That Makes Suspense Possible</h3><blockquote><strong><em>Series:</em></strong><em> How React Works Under the Hood </em><strong><em>Part 1:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-1-motivation-behind-react-fiber-time-slicing-suspense-062e6431d4b0"><em>Motivation Behind React Fiber: Time Slicing &amp; Suspense</em></a><em> </em><strong><em>Part 2:</em></strong><em> </em><a href="https://samabaasi.medium.com/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-98e2f10ec0a7"><em>Why React Had to Build Its Own Execution Engine</em></a><em> </em><strong><em>Part 3:</em></strong><em> </em><a href="https://medium.com/stackademic/how-react-works-part-3-how-react-finds-what-actually-changed-35680df98712"><em>How React Finds What Actually Changed</em></a><em> </em><strong><em>Prerequisites:</em></strong><em> Read Parts 1–3 first.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qdffSdTi2iZSw929YtZIEQ.png" /><figcaption><em>How React Works Under the Hood</em></figcaption></figure><h3>A Question Before We Start</h3><p>Think about how you fetch data in a React component today. You probably do something like this:</p><pre>function UserProfile({ id }) {<br>  const [user, setUser] = useState(null);<br>  const [loading, setLoading] = useState(true);<br><br>  useEffect(() =&gt; {<br>      fetchUser(id).then(data =&gt; {<br>        setUser(data);<br>        setLoading(false);<br>      });<br>    }, [id]);<br>    if (loading) return &lt;Spinner /&gt;;<br>    return &lt;div&gt;{user.name}&lt;/div&gt;;<br>  }</pre><p>It works. But look at what you had to do: manage two pieces of state, write an effect, handle the loading condition manually, repeat this pattern in every component that fetches data.</p><p>Now look at what Suspense lets you write instead:</p><pre>function UserProfile({ id }) {<br>  const user = fetchUser(id); // just... read the data<br>  return &lt;div&gt;{user.name}&lt;/div&gt;;<br>}<br><br>&lt;Suspense fallback={&lt;Spinner /&gt;}&gt;<br>  &lt;UserProfile id={1} /&gt;<br>&lt;/Suspense&gt;</pre><p>No loading state. No effect. No condition. The component just reads data as if it’s already there. If it isn’t, React handles the waiting — including showing the spinner — automatically.</p><p>How is this possible? How can a component just “pause” mid-render and resume when data arrives?</p><p>The answer involves a concept called <strong>algebraic effects</strong> — and understanding it will make Suspense, ErrorBoundary, and some of React’s most surprising behaviors finally make sense.</p><h3>Start With Something You Already Know: throw</h3><p>Algebraic effects sound academic. But Dan Abramov wrote one of the best explanations of them, and he started with something every JavaScript developer already knows: try / catch.</p><p>Here’s how throw works:</p><pre>function getName(user) {<br>  if (user.name === null) {<br>    throw new Error(&#39;no name&#39;);<br>  }<br>  return user.name;<br>}<br><br>try {<br>  getName(user);<br>} catch (err) {<br>  console.log(&#39;handled:&#39;, err.message);<br>}</pre><p>Notice what throw actually does. It doesn&#39;t decide what happens when the error occurs. It just <em>signals</em> that something happened. The catch block somewhere up the call stack is what decides the behavior.</p><p>This separation — <em>signaling</em> something vs <em>handling</em> it — is the core idea.</p><p>And notice: it doesn’t matter how deep getName is in the call stack. It could be called 100 functions deep. throw bypasses all of them and finds the nearest catch. The middle layers don&#39;t have to do anything. They don&#39;t even know about the error.</p><h3>The Problem With try / catch: No Coming Back</h3><p>try / catch has one limitation that matters a lot.</p><p>When you throw, execution stops at that point. The catch block runs. And that&#39;s it — you can never go back to the line that threw and continue from there.</p><pre>function getName(user) {<br>  if (user.name === null) {<br>    throw new Error(&#39;no name&#39;);<br>    // ← once you throw, you can NEVER resume here<br>  }<br>  return user.name;<br>}</pre><p>For errors, this makes sense. But what if you wanted to <em>ask</em> the surrounding context a question and then <em>continue</em> based on the answer?</p><p>Like: <em>“I need this user’s name. I don’t have it. Can someone provide it? I’ll wait here.”</em></p><p>That’s what algebraic effects enable. Dan Abramov described it as a “resumable try / catch.&quot;</p><h3>Algebraic Effects: A Resumable try / catch</h3><p>Algebraic effects don’t exist in JavaScript yet. They’re a research concept — supported only by a handful of languages built specifically to explore the idea. But Dan Abramov explained them using a hypothetical JavaScript dialect, and his explanation is the clearest one I’ve found. Let’s walk through it.</p><p>Imagine JavaScript had two new keywords: perform and resume with.</p><p>perform works like throw — it signals something to the surrounding context and finds the nearest handler up the call stack. The crucial difference: it doesn&#39;t stop execution. The handler can call resume with to jump <em>back</em> to exactly where perform was called and continue from there, passing a value back in.</p><p>Here’s Dan’s example. We have a function that needs a user’s name but doesn’t have it:</p><pre>// Hypothetical JavaScript — this doesn&#39;t exist yet<br>function getName(user) {<br>  if (user.name === null) {<br>    // 1. Signal: &quot;I need a name&quot; — but don&#39;t stop<br>    const name = perform &#39;ask_name&#39;;<br>    // 4. Resume here — name is now &#39;Arya Stark&#39;<br>  }<br>  return user.name;<br>}<br><br>try {<br>  makeFriends(arya, gendry);<br>} handle (effect) {<br>  if (effect === &#39;ask_name&#39;) {<br>    // 2. Handler catches it - like catch<br>    // 3. Resume with a value - unlike catch<br>    resume with &#39;Arya Stark&#39;;<br>  }<br>}</pre><p>The numbered steps show what happens: perform signals (1), the handler catches it (2), the handler provides a value (3), execution jumps back to where perform was and continues with that value (4).</p><p>Notice what this doesn’t require. makeFriends — the function between getName and the handler — doesn&#39;t know anything about this. It didn&#39;t have to be modified. It didn&#39;t have to pass anything through. The effect just bypassed it entirely, exactly like throw bypasses intermediate functions on its way to catch.</p><p>This is what Dan calls <strong>“a function has no color.”</strong> With async/await, making one function async infects every function above it — they all have to become async too. With algebraic effects, a deeply nested function can perform an effect without any of the layers above it caring. The handler somewhere at the top decides what to do, and execution resumes as if nothing unusual happened.</p><h3>What makes this more powerful than try / catch</h3><p>With regular try / catch, the handler is always <em>synchronous</em> and can never return a value back to the thrower. With algebraic effects, the handler can:</p><ul><li>Respond synchronously — resume with an immediate value</li><li>Respond asynchronously — call resume with inside a setTimeout or after a fetch</li><li>The code that did perform doesn&#39;t need to know which one happened</li></ul><p>This last point is the key. The same getName function works whether the handler looks up the name from memory, fetches it from a database, or generates it randomly. The function that <em>needs</em> the effect is completely decoupled from how the effect is <em>fulfilled</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ApStf5iEmvp4ullvT_1seg.png" /></figure><p>That decoupling — signal here, handle somewhere above, resume back here — is the entire concept. And it maps almost exactly onto what React does with Suspense.</p><h3>Suspense Is React’s Implementation of This Idea</h3><p>JavaScript doesn’t actually have perform and resume with. They don&#39;t exist. But React simulates this pattern using the tools JavaScript does have: throw and Fiber.</p><p>Here’s what actually happens when a component suspends:</p><p><strong>The component throws a Promise.</strong></p><p>When fetchUser(id) is called and the data isn&#39;t in the cache yet, instead of returning null or undefined, it throws a Promise — the Promise that will resolve when the data arrives.</p><pre>// Inside the data fetching library<br>function fetchUser(id) {<br>  const cached = cache.get(id);<br><br>if (cached === &#39;pending&#39;) {<br>    throw promise; // ← this is the &quot;perform&quot;<br>  }<br>  if (cached === &#39;resolved&#39;) {<br>    return cache.get(id); // ← data is ready, return normally<br>  }<br>  // Start the fetch, mark as pending, throw<br>  const promise = fetch(`/users/${id}`).then(data =&gt; cache.set(id, data));<br>  cache.set(id, &#39;pending&#39;);<br>  throw promise;<br>}</pre><p><strong>React catches the thrown Promise.</strong></p><p>Because React’s render loop runs inside a try / catch, it catches the thrown Promise. This is the &quot;handler&quot; in the algebraic effects model — React itself acts as the effect handler.</p><p><strong>React shows the fallback.</strong></p><p>React walks up the Fiber tree to find the nearest &lt;Suspense&gt; boundary and shows its fallback — the &lt;Spinner /&gt; — while waiting.</p><p><strong>When the Promise resolves, React retries.</strong></p><p>When the data arrives, React re-renders the component from the &lt;Suspense&gt; boundary downward. This time, the cache has the data. fetchUser returns normally instead of throwing. The component renders successfully.</p><p>As Sam Galson described it:</p><blockquote>“A component is able to suspend the fiber it is running in by throwing a promise, which is caught and handled by the framework. When the promise resolves, its value is added to a cache, the fiber is restarted, and the next time the data is requested it is accessed synchronously from cache.”</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RKjuGVcdgZr4ZVL4NtZkBw.png" /></figure><h3>Why This Only Works Because of Fiber</h3><p>Here’s the critical question: when a component throws mid-render, wouldn’t all the work React did to reach that point be lost?</p><p>With the old synchronous call stack — yes. Throwing would unwind the entire stack, destroying every local variable and stack frame on the way up.</p><p>But React uses Fiber, not the native call stack, to track rendering work. Fibers are plain JavaScript objects — they survive the throw. They sit in memory exactly as they were.</p><p>When React catches the thrown Promise and shows the fallback, all the Fiber work up to that point is preserved. When the data arrives and React retries, it resumes from the Suspense boundary without re-doing all the ancestor work.</p><p>This is what Sam Galson meant when he wrote that the throw-based approach “isn’t problematic for React because it relies on fibers not the native call stack to track program execution.”</p><p>Fiber made algebraic-effect-style control flow possible in JavaScript. Without Fiber, throwing a Promise would be catastrophic. With Fiber, it’s just a pause.</p><h3>ErrorBoundary Is the Same Pattern</h3><p>Once you understand Suspense through this lens, ErrorBoundary becomes obvious — it’s the same mechanism, just for actual errors instead of Promises.</p><p>When a component throws an error during rendering, React walks up the Fiber tree looking for the nearest class component that implements getDerivedStateFromError. When it finds one, it re-renders that component with error state, showing the fallback UI instead of the crashed subtree.</p><p>The component that threw didn’t decide what happens — it just threw. The ErrorBoundary somewhere up the tree decided the behavior. Sound familiar?</p><p>That’s the same separation as perform and handle. The throwing component signals something happened. The handler decides what to do.</p><pre>class ErrorBoundary extends React.Component {<br>  static getDerivedStateFromError(error) {<br>    return { hasError: true }; // ← this is the &quot;handle&quot;<br>  }<br><br>render() {<br>    if (this.state.hasError) {<br>      return &lt;h1&gt;Something went wrong.&lt;/h1&gt;;<br>    }<br>    return this.props.children;<br>  }<br>}</pre><p>The key difference from Suspense: ErrorBoundary doesn’t resume. Once a component threw an error, that render is abandoned. The boundary re-renders with fallback. Suspense, by contrast, genuinely retries the original render when the data arrives.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GfIw5LKR8_IjMO5nUZ0E1g.png" /></figure><h3>The Bigger Picture</h3><p>Dan Abramov wrote in his algebraic effects article that the React team — specifically Sebastian Markbåge — had been using algebraic effects as a mental model for years:</p><blockquote>“My colleague Sebastian kept referring to them as a mental model for some things we do inside of React. At some point, it became a running joke on the React team.”</blockquote><p>Hooks follow the same idea too. When you call useState inside a component, that component doesn&#39;t know or care where its state is actually stored. It just performs a &quot;request for state&quot; and React — the handler — provides it from the Fiber&#39;s hook linked list. The component doesn&#39;t reach into React&#39;s internals. React provides the value through the call.</p><p>This separation — components declaring <em>what they need</em>, React deciding <em>how to provide it</em> — is algebraic effects thinking applied to a UI library. And it’s why React’s API feels declarative even when the underlying behavior is complex.</p><h3>What’s Coming in Part 5</h3><p>In Part 5 we look at the React lifecycle from the inside — initial mount, re-render, useEffect, and useLayoutEffect. We&#39;ll trace exactly when each fires, why the order is what it is, and what &quot;after the browser paints&quot; actually means.</p><h3>🎬 Watch These</h3><p><strong>Sam Galson — </strong><a href="https://www.youtube.com/watch?v=5OPyjrKytLU"><strong>Magic in the web of it: coroutines, continuations, fibers | React Advanced London</strong></a> The CS foundation — fibers, coroutines, and how React uses the throw/catch mechanism to simulate algebraic effects. The source for the Fiber + algebraic effects connection in this article.</p><p><strong>Matheus Albuquerque — </strong><a href="https://www.youtube.com/watch?v=pm5rQBL5sk4&amp;t=766s"><strong>Inside Fiber | React Summit 2022</strong></a> Start at <a href="https://www.youtube.com/watch?v=pm5rQBL5sk4&amp;t=766s">12:46</a> — Matheus shows how Context API was designed using algebraic effects thinking, and how Suspense connects to it.</p><h3>🙏 Sources &amp; Thanks</h3><ul><li><strong>Dan Abramov</strong> — <a href="https://overreacted.io/algebraic-effects-for-the-rest-of-us/">Algebraic Effects for the Rest of Us</a> on overreacted.io. The full three-step build in this article — try/catch → limitation → perform/resume with — follows Dan&#39;s progression directly. The Arya Stark example, the &quot;resumable try/catch&quot; framing, the &quot;function has no color&quot; insight about async infection, the async handler example, and the quote about Sebastian Markbåge using algebraic effects as a running joke on the React team — all come verbatim or paraphrased from this article. Read it after this one.</li><li><strong>Sam Galson</strong> — <a href="https://www.youtube.com/watch?v=5OPyjrKytLU">Magic in the web of it (talk)</a> and <a href="https://www.yld.com/blog/continuations-coroutines-fibers-effects">Continuations, coroutines, fibers, effects (article)</a>. The quote about how “a component is able to suspend the fiber it is running in by throwing a promise” and the throw-handle-resume pattern description come directly from Sam’s article.</li><li><strong>Sebastian Markbåge</strong> — for the original “Poor man’s algebraic effects” gist that modeled the technique later used by React Suspense, cited in Sam Galson’s article.</li><li><a href="https://jser.dev/"><strong>jser.dev</strong></a> — source verification of how throwException, the Suspense retry mechanism, and ErrorBoundary recovery actually work in the React codebase.</li></ul><p><em>Part 5 is next — the React lifecycle from the inside: when </em><em>useEffect and </em><em>useLayoutEffect fire, and why the order is exactly what it is. 🔧</em></p><p><strong>Tags:</strong> #react #javascript #webdev #tutorial</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb96d5a7ad97" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/how-react-works-part-4-the-idea-that-makes-suspense-possible-eb96d5a7ad97">How React Works (Part 4)? The Idea That Makes Suspense Possible</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Trap of Perfectionism: Do Easy Peasy Lemon Squeezy]]></title>
            <link>https://samabaasi.medium.com/the-trap-of-perfectionism-do-easy-peasy-lemon-squeezy-ec7786d9f5c9?source=rss-2b516377a244------2</link>
            <guid isPermaLink="false">https://medium.com/p/ec7786d9f5c9</guid>
            <category><![CDATA[webdev]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[content-creation]]></category>
            <category><![CDATA[overcoming-perfectionism]]></category>
            <dc:creator><![CDATA[Sam Abaasi]]></dc:creator>
            <pubDate>Sun, 03 May 2026 12:27:27 GMT</pubDate>
            <atom:updated>2026-05-03T12:27:27.674Z</atom:updated>
            <content:encoded><![CDATA[<h3>I’ve spent years writing articles — and almost as many years not publishing them.</h3><p>Not because I had nothing to say. I had <em>everything</em> to say. Every time I solved a hard problem or finally understood something deeply — I wrote it down. That feeling of a light turning on after days of confusion? I wanted to give that to someone else.</p><p>But perfectionism had other plans.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t-jBR6Y-sEaBeDaL6lDWsg.png" /></figure><h3>Drafts Everywhere</h3><p>Medium. DEV Community. Google Docs. Chunks of paper stuffed inside books, sitting at the bottom of my laptop bag, falling out when I least expect it.</p><p>But I never wanted to publish something incomplete. No article without proper diagrams. No explanation that left gaps. It had to be whole — or it wasn’t going out.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wVkT2UWR9QkN1AQXM-n96w.png" /><figcaption>medium.com/@samabaasi</figcaption></figure><p>So every draft just sat there — waiting for the perfect diagram, the perfect depth of knowledge, the feeling that I truly understood <em>every</em> part of the topic before I had the right to explain any of it.</p><p>Three years of “not yet.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zNF-O-NscyRsIYtHsAqPlQ.png" /><figcaption>Dev.to/samabaasi</figcaption></figure><h3>What I Was Perfectionist About</h3><p>Not grammar. Not formatting. Something deeper.</p><p>I wanted to make it <strong>easy to understand</strong> — the kind of explanation where someone finishes and thinks <em>why didn’t anyone explain it like this before?</em> I wanted <strong>great visualizations</strong> that make things <em>clear</em>. I wanted to <strong>cover everything</strong> so no one gets lost halfway.</p><p>Underneath all of that was a quiet fear: the fear of not being good enough. Of not actually helping someone the way they needed.</p><h3>The Thing Jack Harrington Says</h3><p>Jack Harrington says it in almost every video:</p><blockquote><strong><em>Easy peasy lemon squeezy.</em></strong></blockquote><p>At some point I actually <em>heard</em> what he was saying — if you understand something well enough, you can make it feel light for someone else. The complexity is real. The explanation doesn’t have to be heavy.</p><p>And he uses tools. He doesn’t spend three days hand-crafting a perfect diagram when ChatGPT can make a clear one in five minutes.</p><p>So I started doing the same. And suddenly the wall was gone.</p><p>The goal was never to be perfect. It was always that moment — when something becomes clear for someone. A perfect article in a draft helps nobody. An honest one out in the world might change how someone thinks forever.</p><h3>I Cut the Code. I Kept the Story.</h3><p>I stopped trying to cover everything. Instead I focused on the <strong>problem-solution mindset</strong> — don’t just show the answer, show the <em>journey</em>. The steps, the wrong turns, how the solution was actually found.</p><p>That’s how I learn best. Not from clean final answers — from watching someone walk the path. You can see it in my React series: articles that don’t just explain <em>how</em> React works, but trace <em>why</em> each piece exists. The journey, not just the destination.</p><h3>What Finally Got Published</h3><p>Two series. Many articles. Years of drafts — finally out.</p><p><strong>⚛️ How React Works Under the Hood</strong> — 9 parts. From why Fiber exists to Server Components and hydration. The series I most wanted to find when I was learning and never could.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qdffSdTi2iZSw929YtZIEQ.png" /><figcaption><strong>How React Works Under the Hood</strong></figcaption></figure><p><strong>🔧 Extending bpmn-io Form-JS Beyond Its Limits</strong> — 23 parts. The architecture the official docs never wrote. Everything I had to discover through trial and error — so you don’t have to.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UVYdFI7--Qrf4Wr-obEvgA.png" /><figcaption><strong>Extending bpmn-io Form-JS Beyond Its Limits</strong></figcaption></figure><h3>The Real Lesson</h3><p>Wanting clarity, great visuals, and depth — none of that is wrong.</p><p>But perfectionism becomes a trap the moment it stops making the work <em>better</em> and starts stopping the work from <em>existing.</em></p><p>So — what’s yours? What have you been sitting on because it’s just not ready yet?</p><p><em>Easy peasy lemon squeezy — not because it’s easy. Because you did the hard work, so the reader doesn’t have to do it alone.</em></p><p><em>Sam Abaasi — Frontend Engineer</em> <a href="https://medium.com/@samabaasi">Medium</a> · <a href="https://dev.to/samabaasi">DEV Community</a> · <a href="https://www.linkedin.com/in/samabaasi/">LinkedIn</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ec7786d9f5c9" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>