Skip to main content
Dublin Library

The Publishing Project

Programming notes and principles

 

Over the years I've collected a set of principles and mental models that I constantly try to apply when writing code (successfully and unsuccessfully). These are not specific to any one programming language or framework, but rather general guidelines that help improve code quality, maintainability, and developer productivity.

This post covers the background of common architectural rules, details specific principles and models, and provides practical code examples to illustrate their application.

Background #

In software development, engineering teams often encounter conflicting advice regarding code structure and optimization. The most common maxims include:

Don't Repeat Yourself (DRY)
This principle prevents code duplication. It advocates replacing duplicate logic with shared abstractions or using data normalization to avoid redundancy.
You Aren't Gonna Need It (YAGNI)
This warns against building features or abstractions for hypothetical future use cases. Developers frequently find it tempting to build a complex system that "might be useful later," but this often leads to wasted effort and unnecessary complexity.
Premature Optimization is the root of all evil
This suggests that optimizing code before it becomes a measurable bottleneck wastes resources. Developers should write the code first without optimization. Once the code functions and processes real data, teams can evaluate whether optimizations are necessary.

The Rule of Three (Pragmatic Abstraction) #

Balancing DRY and YAGNI is difficult. The Rule of Three offers a concrete trigger point for determining when to stop copying code and start abstracting it.

The Principle #

Do not refactor, abstract, or generalize functionality until you have written the exact same code three times.

  • First time: Write the code to solve the immediate problem. Do not design for reuse. If the task requires a specific button, build only that specific button.
  • Second time: Copy and paste the code. Duplicate the logic and make the minor changes needed for the second use case. While duplication feels wrong, it remains cheaper than implementing the wrong abstraction.
  • Third time: You now have three distinct examples of how this logic operates. You possess enough context to spot actual patterns versus coincidental similarities. Now—and only now—refactor the three instances into a single, shared abstraction.

Why it works #

Abstracting too early (on the second instance) forces developers to guess the shared logic. This results in rigid structures based on coincidence rather than actual architectural patterns.

Waiting for the third instance is crucial because dismantling a flawed abstraction requires significantly more effort than creating it in the first place. When developers author a new abstraction, they typically work in isolation. However, once an abstraction exists in a shared codebase, dependency entanglement and cognitive load make undoing that decision a massive undertaking:

  • The Expanding Blast Radius: A shared abstraction creates tight coupling across an application. Modifying or deleting a widely used component requires auditing, refactoring, and testing every single implementation to prevent regressions.
  • The "Boolean Flag" Trap: Flawed abstractions rarely fail immediately. As new requirements emerge, developers force the abstraction to accommodate them by adding conditional boolean flags (like isNews or hideAvatar). Over time, this creates a tangled web of conditional logic that is exceptionally difficult to reverse-engineer.
  • Fear of Regression: When confronted with a massive, complicated abstraction, developers often fear breaking existing features. This fear encourages adding yet another workaround rather than taking the time to safely dismantle the broken component.
  • Migration Logistics: Creating code is additive, but dismantling code requires a coordinated migration. Developers must create new, correct components, maintain the legacy component, incrementally migrate existing implementations, and eventually deprecate the old code.

Abstracting too early (on the second instance) forces developers to guess the shared logic. This results in rigid structures based on coincidence rather than actual architectural patterns.

The Cost of "Un-Abstracting" #

Dismantling a flawed abstraction takes significantly more effort than creating a new one. A bad abstraction forces developers to add increasingly complex configuration flags (like boolean props) to accommodate new features, eventually creating brittle code that no one fully understands.

Example of Premature Abstraction

Consider a scenario involving a UserCard and a ProductCard. Because they look visually similar (title, image, subtitle), a developer merges them into a GenericCard immediately.

  • Phase 1: The component works fine. GenericCard accepts a title and an image.
  • Phase 2: The product view needs a "Buy" button. The developer adds if (type === 'product') to GenericCard.
  • Phase 3: The user view needs an avatar with a green "online" dot. The developer adds if (type === 'user').
  • Phase 4: A news feed introduces a NewsCard requiring a date but no image. The GenericCard props now include: showImage, isNews, hasButton, date, and onlineStatus.

The resulting component contains tangled conditional logic, rendering it brittle and difficult to maintain. By waiting for the Rule of Three, the developer might have realized these distinct entities only share a common styling wrapper or CSS class, not a complete component structure.

TypeScript

// ❌ PREMATURE ABSTRACTION (BAD)
// This component knows too much. Notice how many optional props (?)
// are needed to support all the different use cases.

import React from 'react';

interface GenericCardProps {
  title: string;
  image?: string;
  subtitle?: string;
  type: 'product' | 'user' | 'news';
  // Specific to Product
  onBuy?: () => void;
  // Specific to User
  onlineStatus?: 'online' | 'offline' | 'away';
  // Specific to News
  date?: string;
}

export const GenericCard: React.FC<GenericCardProps> = ({
  title,
  image,
  subtitle,
  type,
  onBuy,
  onlineStatus,
  date
}) => {
  return (
    <div className="card">
      {image && <img src={image} alt="" />}

      <div className="content">
        <h3>{title}</h3>
        {subtitle && <p>{subtitle}</p>}

        {/* Conditional Logic Hell */}
        {type === 'product' && onBuy && (
          <button onClick={onBuy}>Buy Now</button>
        )}

        {type === 'user' && onlineStatus && (
          <div className={`status-dot ${onlineStatus}`} />
        )}

        {type === 'news' && date && (
           <span className="date">{date}</span>
        )}
      </div>
    </div>
  );
};

JavaScript

// ❌ PREMATURE ABSTRACTION (BAD)
// This component knows too much. Notice how many optional props
// are needed to support all the different use cases.

import React from 'react';

export const GenericCard = ({
  title,
  image,
  subtitle,
  type,
  onBuy,
  onlineStatus,
  date
}) => {
  return (
    <div className="card">
      {image && <img src={image} alt="" />}

      <div className="content">
        <h3>{title}</h3>
        {subtitle && <p>{subtitle}</p>}

        {/* Conditional Logic Hell */}
        {type === 'product' && onBuy && (
          <button onClick={onBuy}>Buy Now</button>
        )}

        {type === 'user' && onlineStatus && (
          <div className={`status-dot ${onlineStatus}`} />
        )}

        {type === 'news' && date && (
           <span className="date">{date}</span>
        )}
      </div>
    </div>
  );
};

The Priority Triad: Work, Right, Fast #

When authoring a new feature, developers often feel internal pressure to write perfectly optimized, clean, and elegant code on the first pass. This perfectionism paralyzes progress and leads to "analysis paralysis."

Instead, follow this strict, linear order of operations, widely attributed to Kent Beck:

Make it Work #

Focus solely on solving the problem. The code can lack elegance. Developers can place all logic in one file or temporarily use global variables. The goal is to prove the solution is possible and to get the feature functioning. This acts as "prototyping in production."

  • Mental Check: "Does clicking the button actually save the data?"
  • Action: Temporarily ignore linting rules, write long functions, and use console.log generously to verify flow.

Make it Right #

Once the feature works, clean up the implementation. This phase should consume 80% of development time.

  • Refactor: Break the code into smaller functions and modular components.
  • Guard Rails: Handle edge cases (e.g., handling null data or network failures).
  • Clarity: Update variable names to be descriptive. Replace vague variables like x and data with explicit names like userProfile and fetchedProductList.
  • Robustness: Add strict types (TypeScript) and author unit and integration tests.
  • Accessibility: Ensure keyboard navigation and screen reader support function correctly.

The mental check becomes: "Is this code reliable, readable, and maintainable?"

Make it Fast #

Address performance only after the code is working and reliable. Often, the process of "Making it Right" naturally resolves performance issues by organizing logic more effectively.

  • Measure First: Do not guess at performance bottlenecks. Use the Chrome Profiler or React DevTools to identify actual performance drains.
  • Optimize: Implement memoization, virtualization, or algorithm improvements where data dictates the need.

The final mental check: "Is the user experience sluggish?"

Key Takeaway #

Never attempt to "Make it Fast" before you "Make it Work." Developers cannot optimize code that does not yet exist. Furthermore, optimizing buggy code simply results in highly efficient bugs.

Gall's Law & The "Crappy" First Version #

Gall's Law states:

"A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work."

This principle explains why software "rewrites" frequently fail. Designing a complex system (like a massive dashboard application) from scratch to handle every possible future scenario leads to failure. The resulting application becomes buggy, difficult to maintain, and consistently misses delivery deadlines because the team attempts to solve theoretical problems.

The Strategy #

Embrace the MVP (Minimum Viable Product): Build the smallest, simplest version of the system that provides concrete value. It might lack visual polish, but it must function.

Iterate: Add complexity layer by layer.

  • Step 1: Create a button that logs "Clicked" to the console.
  • Step 2: Wire the button to call the API.
  • Step 3: Implement success and error state handling for the button.
  • Step 4: Add a loading spinner to the button's UI.
  • Step 5: Introduce optimistic UI updates.

Avoid the "Second System Effect": This occurs when a team rewrites a legacy system and attempts to immediately include every feature ever requested. This approach consistently leads to project bloat and failure. Start simple, verify it works, and then evolve the architecture.

Idempotency #

The Principle #

A function is idempotent if it produces the exact same result when called multiple times with the same arguments. The operation can repeat without changing the result beyond the initial application.

Examples:

  • Idempotent (Good): "hello".toUpperCase() consistently returns "HELLO".
  • Idempotent (Good): Setting a user's status with user.isActive = true. Running this line 100 times results in the user remaining active. The state is stable.
  • NOT Idempotent (Risky): Toggling a boolean via user.isActive = !user.isActive. Running this twice makes the user active, then inactive. The outcome strictly depends on the execution count.
  • NOT Idempotent (Risky): Sending a generic POST request that creates a duplicate record in the database every time a user double-clicks the "Submit" button due to network latency.

Code Example: Shopping Cart #

This example illustrates the danger of non-idempotent logic when a user clicks a button multiple times (e.g., encountering a slow network and "rage clicking").

TypeScript

interface CartItem {
  id: string;
  name: string;
  price: number;
}

// ❌ NOT IDEMPOTENT (BAD)
// If the user clicks "Add" twice quickly, the application adds the item twice.
function addItemToCartBad(cart: CartItem[], newItem: CartItem): CartItem[] {
  return [...cart, newItem];
}

// ✅ IDEMPOTENT (GOOD)
// Regardless of how many times the user clicks, the item appears only once.
// The function checks if the result already exists before applying the change.
function addItemToCartGood(cart: CartItem[], newItem: CartItem): CartItem[] {
  const exists = cart.find(item => item.id === newItem.id);

  if (exists) {
    return cart;
  }

  return [...cart, newItem];
}

JavaScript

// ❌ NOT IDEMPOTENT (BAD)
// If the user clicks "Add" twice quickly, the application adds the item twice.
function addItemToCartBad(cart, newItem) {
  return [...cart, newItem];
}

// ✅ IDEMPOTENT (GOOD)
// Regardless of how many times the user clicks, the item appears only once.
// The function checks if the result already exists before applying the change.
function addItemToCartGood(cart, newItem) {
  const exists = cart.find(item => item.id === newItem.id);

  if (exists) {
    return cart;
  }

  return [...cart, newItem];
}

Why it matters for Front-End #

Idempotency is crucial for stability in UI development, particularly with libraries like React. Specific areas of concern include:

  • Debugging: Developers can treat an idempotent function as a "black box." It removes the need to track the application's history (e.g., "Did the user click this twice?") to understand the function's output.
  • Resilience: If a network request fails and automatically retries, an idempotent API endpoint ensures the system does not create double charges or duplicate database entries. Ideally, API PUT requests should replace the resource entirely, rather than appending to it.
  • React useEffect: React 18's Strict Mode intentionally executes effects twice during development to stress-test components. If setup and cleanup logic lacks idempotency, the UI breaks (e.g., opening two chat connections simultaneously, or fetching data twice and duplicating list items).

Single Responsibility Principle (SRP) #

The SRP Principle #

A function, module, or component should have only one reason to change. Often phrased as: "Do one thing and do it well."

The Litmus Test #

Attempt to describe what a function or component does using a single sentence.

If the description requires the word "and," it likely violates SRP.

  • Bad: "This function fetches the user and filters for active users and updates the DOM and handles error toasts."
  • Good: "This function fetches the user data."

Practical Application #

Consider an API utility file within a front-end project.

  • Violation: A function getFormattedUserData() that calls the API, reformats the date string from ISO to a human-readable format, and sorts the returned list by name.
  • Why it fails: Developers must edit this file if the API URL changes. They must edit it if design requirements alter the date format. They must edit it again if product requirements change the default sorting behavior to "Last Active."
  • Solution: Split it into composed functions
    • fetchUsers(): Handles only the network request.
    • formatUserDate(): Handles only the date formatting logic.
    • sortUsers(): Handles only the sorting algorithms.

Decoupling these responsibilities isolates change. If the API endpoint changes, the developer only risks breaking the fetcher, preserving the integrity of the date formatter. This architectural pattern makes testing significantly easier by allowing developers to test the formatUserDate function without mocking an entire network call.

Code Example

This code translates the "API Utility" scenario into practical application.

TypeScript

interface ApiUser {
  id: string;
  name: string;
  joinedAt: string; // ISO Date String "2023-01-01T00:00:00Z"
}

interface FormattedUser extends ApiUser {
  joined: string; // Formatted date "1/1/2023"
}

// ❌ VIOLATION: Mixed Responsibilities
// This function is fragile. It breaks if the API changes OR if the date format requirements change.
async function getFormattedUserDataBad(): Promise<FormattedUser[]> {
  // Responsibility 1: Fetching
  const response = await fetch('https://api.example.com/users');
  const users: ApiUser[] = await response.json();

  // Responsibility 2: Formatting AND Responsibility 3: Sorting
  return users
    .map(user => ({
      ...user,
      joined: new Date(user.joinedAt).toLocaleDateString()
    }))
    .sort((a, b) => a.name.localeCompare(b.name));
}

// ✅ FIXED: Composed Single-Purpose Functions

// Responsibility 1: Fetching
// Only changes if the API endpoint or auth logic changes
async function fetchUsers(): Promise<ApiUser[]> {
  const response = await fetch('[https://api.example.com/users](https://api.example.com/users)');
  return response.json();
}

// Responsibility 2: Formatting
// Only changes if the UI design (date format) changes
function formatUserDates(users: ApiUser[]): FormattedUser[] {
  return users.map(user => ({
    ...user,
    joined: new Date(user.joinedAt).toLocaleDateString()
  }));
}

// Responsibility 3: Sorting
// Only changes if the sorting requirement changes (e.g., sort by ID instead of Name)
function sortUsersByName(users: FormattedUser[]): FormattedUser[] {
  return [...users].sort((a, b) => a.name.localeCompare(b.name));
}

// Orchestrator: Connects the pieces without implementing them
// This serves as the "glue" code.
async function getFormattedUserDataGood(): Promise<FormattedUser[]> {
  const users = await fetchUsers();
  const formatted = formatUserDates(users);
  return sortUsersByName(formatted);
}

JavaScript

// ❌ VIOLATION: Mixed Responsibilities
// This function is fragile. It breaks if the API changes OR if the date format requirements change.
async function getFormattedUserDataBad() {
  // Responsibility 1: Fetching
  const response = await fetch('https://api.example.com/users');
  const users = await response.json();

  // Responsibility 2: Formatting AND Responsibility 3: Sorting
  return users
    .map(user => ({
      ...user,
      joined: new Date(user.joinedAt).toLocaleDateString()
    }))
    .sort((a, b) => a.name.localeCompare(b.name));
}

// ✅ FIXED: Composed Single-Purpose Functions

// Responsibility 1: Fetching
// Only changes if the API endpoint or auth logic changes
async function fetchUsers() {
  const response = await fetch('https://api.example.com/users');
  return response.json();
}

// Responsibility 2: Formatting
// Only changes if the UI design (date format) changes
function formatUserDates(users) {
  return users.map(user => ({
    ...user,
    joined: new Date(user.joinedAt).toLocaleDateString()
  }));
}

// Responsibility 3: Sorting
// Only changes if the sorting requirement changes (e.g., sort by ID instead of Name)
function sortUsersByName(users) {
  return [...users].sort((a, b) => a.name.localeCompare(b.name));
}

// Orchestrator: Connects the pieces without implementing them
// This serves as the "glue" code.
async function getFormattedUserDataGood() {
  const users = await fetchUsers();
  const formatted = formatUserDates(users);
  return sortUsersByName(formatted);
}

Single Level of Abstraction #

This concept serves as a subtle but powerful sibling to SRP. A function should not only focus on one responsibility, but it should also operate at one consistent level of detail.

The Anti-Pattern #

Mixing low-level implementation details (like manually parsing JSON, iterating arrays, or writing direct database queries) with high-level business logic (like "Process User Subscription") within the same function creates cognitive friction.

This forces the reader's brain to context-switch rapidly between "What is the business goal?" (High Level) and "How does the syntax execute?" (Low Level).

The Fix

Break the code into functions that sit at the same level of abstraction. The main function should read smoothly, like a recipe or a table of contents.

TypeScript

interface User {
  id: number;
  name: string;
  email: string;
  hasActiveSubscription: boolean;
}

// Mock services for the example
const db = {
  query: async (sql: string): Promise<User> => ({ id: 1, name: 'John', email: 'john@example.com', hasActiveSubscription: true })
};
const emailClient = {
  send: async (to: string, body: string) => console.log(`Sending to ${to}: ${body}`)
};

// ❌ BAD: Mixed levels of abstraction
async function processUserSubscriptionBad(userId: number) {
  // Level 1: Low-level DB access details
  // The reader has to parse SQL to understand what's happening.
  const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);

  // Level 2: High-level business logic
  if (user.hasActiveSubscription) {

    // Level 1: Low-level string manipulation and external service calls
    // The reader gets lost in fetch headers and JSON stringification.
    const emailBody = `Hi ${user.name}, thanks for staying with us!`;
    await fetch('[https://api.emailservice.com/send](https://api.emailservice.com/send)', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ to: user.email, body: emailBody })
    });
  }
}

// ✅ GOOD: Consistent levels of abstraction

// Low Level: Database interaction
// This function hides the SQL complexity.
async function getUser(id: number): Promise<User> {
  return await db.query(`SELECT * FROM users WHERE id = ${id}`);
}

// Low Level: Email service interaction
// This function hides the HTTP fetch complexity.
async function sendThankYouEmail(user: User): Promise<void> {
  const body = `Hi ${user.name}, thanks for staying with us!`;
  await emailClient.send(user.email, body);
}

// High Level: Orchestration
// This function only knows "Users" and "Emails".
async function processUserSubscriptionGood(userId: number): Promise<void> {
  const user = await getUser(userId);

  if (user.hasActiveSubscription) {
    await sendThankYouEmail(user);
  }
}

JavaScript

// Mock services for the example
const db = {
  query: async (sql) => ({ id: 1, name: 'John', email: 'john@example.com', hasActiveSubscription: true })
};
const emailClient = {
  send: async (to, body) => console.log(`Sending to ${to}: ${body}`)
};

// ❌ BAD: Mixed levels of abstraction
async function processUserSubscriptionBad(userId) {
  // Level 1: Low-level DB access details
  // The reader has to parse SQL to understand what's happening.
  const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);

  // Level 2: High-level business logic
  if (user.hasActiveSubscription) {

    // Level 1: Low-level string manipulation and external service calls
    // The reader gets lost in fetch headers and JSON stringification.
    const emailBody = `Hi ${user.name}, thanks for staying with us!`;
    await fetch('[https://api.emailservice.com/send](https://api.emailservice.com/send)', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ to: user.email, body: emailBody })
    });
  }
}

// ✅ GOOD: Consistent levels of abstraction

// Low Level: Database interaction
// This function hides the SQL complexity.
async function getUser(id) {
  return await db.query(`SELECT * FROM users WHERE id = ${id}`);
}

// Low Level: Email service interaction
// This function hides the HTTP fetch complexity.
async function sendThankYouEmail(user) {
  const body = `Hi ${user.name}, thanks for staying with us!`;
  await emailClient.send(user.email, body);
}

// High Level: Orchestration
// This function only knows "Users" and "Emails".
async function processUserSubscriptionGood(userId) {
  const user = await getUser(userId);

  if (user.hasActiveSubscription) {
    await sendThankYouEmail(user);
  }
}

In the "Good" example, processUserSubscriptionGood functions as the Orchestrator. It reads clearly: "Get the user. If they are active, send an email." If a developer needs to know how the email sends, they can inspect that specific function. This architectural pattern greatly reduces cognitive load and accelerates debugging.

Version Specificity #

When writing code, especially in shared codebases or libraries, developers must remain aware of the execution context. This requires knowing the exact versions of dependencies, frameworks, or languages in use and ensuring the implementation remains compatible.

Why it matters #

A common pitfall occurs when developers copy code from a tutorial without verifying the targeted version. Frameworks evolve constantly; they introduce breaking changes, deprecate legacy features, or establish new paradigms (like React Server Components) that fundamentally alter code execution.

Blindly pasting "modern" code into a "legacy" codebase (or vice versa) invariably leads to baffling errors.

Example: React 19 vs. React 18 #

The following architecture demonstrates a "modern" approach utilizing React Server Actions. This code works perfectly in a Next.js App Router project utilizing React 19 features, but it crashes immediately in a standard React 18 Single Page Application (SPA).

The "Modern" Implementation (React 19 / Next.js) #

The Server Action (The "Backend"): This file runs strictly on the server ('use server').

TypeScript

'use server';

import { revalidatePath } from 'next/cache';

// Mock database (resets when server restarts)
let globalLikeCount = 42;

export async function incrementLikes(): Promise<number> {
  // 1. Simulate a network/database delay
  await new Promise((resolve) => setTimeout(resolve, 500));

  // 2. Mutate the data
  globalLikeCount += 1;

  // 3. Revalidate the path
  revalidatePath('/');
  return globalLikeCount;
}

export async function getLikeCount(): Promise<number> {
  return globalLikeCount;
}

JavaScript

'use server';

import { revalidatePath } from 'next/cache';

// Mock database (resets when server restarts)
let globalLikeCount = 42;

export async function incrementLikes() {
  // 1. Simulate a network/database delay
  await new Promise((resolve) => setTimeout(resolve, 500));

  // 2. Mutate the data
  globalLikeCount += 1;

  // 3. Revalidate the path
  revalidatePath('/');
  return globalLikeCount;
}

export async function getLikeCount() {
  return globalLikeCount;
}

The Client Component (The "Interactivity"): This file runs in the browser ('use client'). Notice the use of useTransition for asynchronous actions.

TypeScript

'use client';

import { useTransition } from 'react';
import { incrementLikes } from '../app/actions';

interface LikeButtonProps {
  count: number;
}

export default function LikeButton({ count }: LikeButtonProps) {
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    startTransition(async () => {
      // Direct server function invocation
      await incrementLikes();
    });
  };

  // Defining the style outside prevents "double curly brace" conflicts
  // with some template engines like Liquid/Nunjucks in static site generators.
  const buttonStyle = {
    opacity: isPending ? 0.5 : 1
  };

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
      style={buttonStyle}
    >
      {isPending ? 'Saving...' : `${count} Likes`}
    </button>
  );
}

JavaScript

'use client';

import { useTransition } from 'react';
import { incrementLikes } from '../app/actions';

export default function LikeButton({ count }) {
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    startTransition(async () => {
      // Direct server function invocation
      await incrementLikes();
    });
  };

  // Defining the style outside prevents "double curly brace" conflicts
  // with some template engines like Liquid/Nunjucks in static site generators.
  const buttonStyle = {
    opacity: isPending ? 0.5 : 1
  };

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
      style={buttonStyle}
    >
      {isPending ? 'Saving...' : `${count} Likes`}
    </button>
  );
}

The Backward Compatibility Trap #

Attempting to paste the code above into a standard React 18 codebase (e.g., standard Vite or Create React App) causes multiple failures:

  • Direct Async Components: React 18 does not support async function Page(). It expects components to return JSX synchronously, not resolve a Promise.
    • Error: Objects are not valid as a React child (found: [object Promise]).
  • Directives: 'use server' and 'use client' serve as instructions for specific bundlers (like Webpack or Turbopack in Next.js). Standard client-side React 18 builds do not separate these execution environments or generate API endpoints automatically.
  • Removed APIs: React 19 removes legacy features that older codebases frequently rely upon.
Feature Status in React 19 Replacement
ReactDOM.render Removed Use ReactDOM.createRoot (introduced in v18).
defaultProps Removed (functional) Use ES6 default parameters: function MyComponent({ val = 10 }).
ref as prop Changed The forwardRef utility is deprecated. Pass ref as a standard prop.

Edit on Github