Beginner

React Fundamentals

React from scratch — JSX, components, styling, hooks, props/state, side effects and forms.

Table of Contents

  1. The Power of React
  2. Setting Up a New Project
  3. Styling Components
  4. Hooks, Props and State
  5. Component Rendering and Side Effects
  6. Conditional Rendering and Shared State
  7. Context and Navigation
  8. User Input and Forms
  9. Server-side React and Next.js
  10. Application Design

1. The Power of React

Single-Page Applications (SPA)

React is an open-source JavaScript library for building user interfaces, primarily in Single-Page Application (SPA) type applications.

Application TypeBehavior
Traditional applicationEach interaction sends an HTTP request to the server which returns a new HTML page
SPAThe complete UI is downloaded in a single request. The browser then handles all navigation

In a SPA, the server is still needed for APIs (data), but not for the user interface. React focuses on the UI layer; other libraries handle the rest.

Virtual DOM and Reconciliation

flowchart TD
    A[State change] --> B[React creates a new Virtual DOM tree]
    B --> C[Diffing: comparison old tree vs new tree]
    C --> D{Any differences?}
    D -- No --> E[No browser DOM update]
    D -- Yes --> F[Minimal instructions sent to browser DOM]
    F --> G[Browser updates only what changed]

Why this matters: Browser DOM updates are expensive in terms of performance. React minimizes these updates through reconciliation.

Concrete demonstration:

  • Pure JavaScript version: the entire div is recreated on each render (impossible to type in an input)
  • React version: only the changing part (date/time) is updated → the input remains intact

Component Structure

flowchart TD
    App --> Banner
    App --> HouseList
    HouseList --> HouseRow1["HouseRow (×N)"]
    HouseRow1 --> Bids
    Bids --> AddBid
    Bids --> BidList

A React component is a JavaScript function that:

  • Receives data via props
  • Maintains internal data via state
  • Returns JSX representing the UI
  • Re-renders automatically when its state changes

JSX — JavaScript eXtension

JSX is a syntactic extension of JavaScript that looks like HTML, but isn’t. It is transformed by a transpiler (Babel or SWC) into React.createElement calls.

// JSX
const Banner = () => (
  <header className="row">
    <div className="col-5">
      <img src="/logo.png" alt="Logo" />
    </div>
    <div className="col-7">
      <h1>Globomantics</h1>
    </div>
  </header>
);

// What Babel generates
const Banner = () =>
  React.createElement(
    'header',
    { className: 'row' },
    React.createElement('div', { className: 'col-5' },
      React.createElement('img', { src: '/logo.png', alt: 'Logo' })
    ),
    React.createElement('div', { className: 'col-7' },
      React.createElement('h1', null, 'Globomantics')
    )
  );

Essential JSX Rules:

RuleHTMLJSX
Class attributeclass="..."className="..."
Style attributestyle="color:red"style={{ color: 'red' }}
Self-closingOptionalRequired: <img />
Single root elementNot requiredRequired (or <>...</>)
JavaScript expressionsN/A{expression}
Comments<!-- -->{/* comment */}

2. Setting Up a New Project

Required Tools

A modern React project requires:

  • Transpiler: transforms JSX → JavaScript (Babel or SWC)
  • Bundler: groups JavaScript files for production
  • Dev Server: serves the application in development with hot reload
  • Build tool: generates optimized production files (minified)

Getting Started with Vite

Vite (pronounced veet, “speed” in French) is the recommended tool for starting a client-side React project.

Prerequisites: Node.js installed (nodejs.org)

# Create a React project with Vite
npm create vite@6

# Choose:
# - Project name: globomantics
# - Framework: React
# - Variant: JavaScript + SWC

cd globomantics
npm install
npm run dev

SWC (Speedy Web Compiler) is a faster transpiler than Babel that:

  • Transforms JSX to JavaScript
  • Updates the browser instantly during development (HMR — Hot Module Replacement)

Vite Commands

CommandDescription
npm run devLaunches the development server with HMR
npm run buildGenerates the production build in /dist
npm run lintChecks the code with ESLint
npm run previewTests the production build locally

Project Structure

globomantics/
├── public/              # Static files (not processed by Vite)
│   └── vite.svg
├── src/                 # Source code (processed by Vite)
│   ├── components/      # Custom components
│   ├── hooks/           # Custom hooks
│   ├── App.jsx          # Root component
│   ├── App.css          # App global styles
│   └── main.jsx         # Entry point
├── index.html           # Root HTML page (loads main.jsx)
├── eslint.config.js     # ESLint configuration
├── vite.config.js       # Vite configuration
└── package.json         # Dependencies and scripts

index.html — Entry point:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Globomantics</title>
  </head>
  <body>
    <div id="root"></div>  <!-- React mounts here -->
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

main.jsx — Application mounting:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

JavaScript Modules

React components use the ES modules system (import/export).

// module.js — Named export
export const doSomething = () => { /* ... */ };
export const doSomethingElse = () => { /* ... */ };

// Another file — Named import
import { doSomething, doSomethingElse } from './module';

// Default export
export default function Banner() { /* ... */ }

// Default import (no curly braces)
import Banner from './components/Banner';

Adding Components

Example Banner.jsx component:

// src/components/Banner.jsx
const Banner = () => {
  return (
    <header className="row mb-4">
      <div className="col-5">
        <img src="/GloboLogo.png" className="logo" alt="Globomantics Logo" />
      </div>
      <div className="col-7 mt-5">
        <h1>Globomantics</h1>
      </div>
    </header>
  );
};

export default Banner;

ESLint and Debugging

ESLint is included in the Vite template. It detects errors and style problems.

  • Install the ESLint extension in VS Code to see errors in real time
  • Configure rules in eslint.config.js
  • For debugging: use VS Code’s Run and Debug tab with Chrome

3. Styling Components

Global Styles

Option 1 — Via index.html:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.x/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/globals.css" />

Option 2 — Via import in a component (recommended):

// src/App.jsx
import './App.css';

Files in src/ are processed by Vite (minified, bundled). Files in public/ are copied as-is to /dist.

Applying CSS Classes

// className instead of class (class is a reserved JavaScript word)
const Banner = () => (
  <header className="row mb-4">
    <div className="col-5">
      <img src="/logo.png" className="logo" alt="Logo" />
    </div>
    <div className="col-7 mt-5">
      <h1 className="themeFontColor">Globomantics</h1>
    </div>
  </header>
);

Multiple classes:

<header className="row mb-4">
// or dynamically:
<td className={`${price < 500000 ? '' : 'text-primary'}`}>

CSS Modules

CSS Modules isolate styles per component. Vite automatically generates unique class names.

/* Banner.module.css */
.logo {
  width: 120px;
}
// Banner.jsx
import { logo as logoClass } from './Banner.module.css';
// or:
import styles from './Banner.module.css';

const Banner = () => (
  <header>
    <img className={logoClass} src="/logo.png" alt="Logo" />
    {/* or: <img className={styles.logo} ... /> */}
  </header>
);

The file must have the .module.css extension to be treated as a CSS Module.

Inline Style Attribute

// Object defined outside the function (avoids recreation on each render)
const subtitleStyle = {
  color: '#6c757d',
  fontSize: '1.2rem',
  fontStyle: 'italic',
};

const Banner = ({ children }) => (
  <header>
    <div style={subtitleStyle}>{children}</div>
  </header>
);

// Direct inline (two braces: JSX expression + object)
<div style={{ color: 'red', fontSize: '14px' }}>Text</div>

Using style inline is not recommended — prefer CSS Modules.


4. Hooks, Props and State

Props

Props allow a parent component to pass data to its children.

// Passing props (HTML attribute syntax)
<Banner headerText="Welcome to Globomantics" count={42} user={userObj} />

// Receiving in the component
const Banner = (props) => {
  return <h1>{props.headerText}</h1>;
};

// With destructuring (recommended)
const Banner = ({ headerText, count, user }) => {
  return <h1>{headerText} — {count}</h1>;
};

Fundamental rule: Props are read-only. A component must never modify its own props.

Prop Types

import PropTypes from 'prop-types';

const Banner = ({ headerText }) => <h1>{headerText}</h1>;

Banner.propTypes = {
  headerText: PropTypes.string.isRequired,
};

Note: PropTypes are deprecated by the React team. The recommendation is to use TypeScript for type safety.

Children Prop

// Usage
<Banner>Welcome to Globomantics</Banner>

// Component Banner
const Banner = ({ children }) => (
  <header>
    <div>{children}</div>
  </header>
);

The children prop contains all content placed between the opening and closing tags of the component.

Fragments and Data Mapping

// Fragment: avoids adding an unnecessary div in the DOM
const HouseList = () => {
  const houses = [
    { id: 1, address: '12 Main St', country: 'USA', price: 450000 },
    { id: 2, address: '7 Oak Ave', country: 'Canada', price: 650000 },
  ];

  return (
    <>
      <h2 className="themeFontColor">Houses for Sale</h2>
      <table className="table table-hover">
        <thead>
          <tr>
            <th>Address</th>
            <th>Country</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>
          {houses.map((house) => (
            <tr key={house.id}>
              <td>{house.address}</td>
              <td>{house.country}</td>
              <td>{house.price}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
};

The Key Prop

// Bad — no key
{houses.map((house) => <HouseRow house={house} />)}

// Good — unique key
{houses.map((house) => (
  <HouseRow key={house.id} house={house} />
))}

// Last resort — index (problematic if order changes)
{houses.map((house, index) => (
  <HouseRow key={index} house={house} />
))}

Why key is required: React uses it to identify each element in a list and optimize updates (reconciliation). Without key, React recreates the entire list on each render.

Component Extraction

// Before: HouseRow inline in HouseList
<tbody>
  {houses.map((h) => (
    <tr key={h.id}>
      <td>{h.address}</td>
      <td>{h.country}</td>
      <td>{h.price}</td>
    </tr>
  ))}
</tbody>

// After: extracted HouseRow component
const HouseRow = ({ house }) => (
  <tr>
    <td>{house.address}</td>
    <td>{house.country}</td>
    <td>{house.price}</td>
  </tr>
);

Hooks — Fundamental Rules

flowchart LR
    A[Rule 1: Top Level] --> B["Call hooks ALWAYS\n(never inside an if, loop, early return)"]
    C[Rule 2: Function Components] --> D["Hooks only inside\nfunction components or custom hooks"]
HookDescription
useStateManages local state in a component
useEffectExecutes side effects after rendering
useContextAccesses a Context
useRefStores a persistent value without triggering re-render
useMemoMemoizes the result of an expensive computation
useCallbackMemoizes a function to avoid re-renders
useReducerAlternative to useState for complex logic
useTransitionMarks a state update as non-urgent
useActionStateManages state linked to a form action (React 19)
useOptimisticOptimistic UI update (React 19)

State with useState

import { useState } from 'react';

const HouseList = () => {
  // [current_value, update_function] = useState(initial_value)
  const [houses, setHouses] = useState([
    { id: 1, address: '12 Main St', country: 'USA', price: 450000 },
    { id: 2, address: '7 Oak Ave', country: 'Canada', price: 650000 },
  ]);

  const addHouse = () => {
    // DON'T modify houses directly!
    // Always provide a new instance to the set function
    setHouses([
      ...houses,  // spread: copies all existing elements
      {
        id: houses.length + 1,
        address: 'New House',
        country: 'France',
        price: 320000,
      },
    ]);
  };

  return (
    <>
      <table className="table table-hover">
        <tbody>
          {houses.map((house) => (
            <HouseRow key={house.id} house={house} />
          ))}
        </tbody>
      </table>
      <button className="btn btn-primary" onClick={addHouse}>
        Add House
      </button>
    </>
  );
};

Critical rule: Never directly modify state. Always use the set function so React detects the change and re-renders the component.


5. Component Rendering and Side Effects

Rendering vs Re-rendering

flowchart LR
    A["Render = Execution\nof the component function"] --> B["React compares\n(Virtual DOM diff)"]
    B --> C["Browser update\n(only the changes)"]

Re-render triggers:

  • Local state change (call to a set function)
  • Change in received props
  • Re-render of the parent component

Pure Functions and React.memo

// Pure component: same props → same JSX returned
const HouseRow = ({ house }) => (
  <tr>
    <td>{house.address}</td>
    <td>{house.price}</td>
  </tr>
);

// Memoization: avoids re-render if props haven't changed
import { memo } from 'react';

const HouseRowMemo = memo(({ house }) => (
  <tr>
    <td>{house.address}</td>
    <td>{house.price}</td>
  </tr>
));

export default HouseRowMemo;

Side Effects and useEffect

A side effect is any operation outside React’s domain: API calls, browser DOM access, timers, etc.

import { useState, useEffect } from 'react';

const HouseList = () => {
  const [houses, setHouses] = useState([]);

  useEffect(() => {
    // This code runs AFTER React has updated the browser
    const fetchHouses = async () => {
      const response = await fetch('http://localhost:6001/houses');
      const data = await response.json();
      setHouses(data);
    };

    fetchHouses();
  }, []); // [] = empty dependency array: runs only on mount

  return (
    <table>
      <tbody>
        {houses.map((house) => (
          <HouseRow key={house.id} house={house} />
        ))}
      </tbody>
    </table>
  );
};

Dependency Array and Infinite Loop

// Without dependency array → runs on EVERY render (dangerous!)
useEffect(() => {
  fetchData();
});

// With empty [] → runs only on MOUNT
useEffect(() => {
  fetchData();
}, [];

// With dependencies → runs when houseId changes
useEffect(() => {
  fetchBids(houseId);
}, [houseId]);
flowchart TD
    A[Component mounts] --> B[useEffect runs]
    B --> C[fetch API → setHouses]
    C --> D[Re-render]
    D --> E{dependency array\nchanged?}
    E -- No --> F[useEffect doesn't run\nNo infinite loop]
    E -- Yes --> B

Suspense and use

React 19 introduces the use function to simplify Promise handling:

import { use, Suspense, useState } from 'react';

// Promise created outside the component
const housesPromise = fetch('http://localhost:6001/houses')
  .then((res) => res.json());

const HouseList = () => {
  // use() suspends the component until the Promise resolves
  const fetchedHouses = use(housesPromise);
  const [houses, setHouses] = useState(fetchedHouses);

  return (
    <table>
      <tbody>
        {houses.map((house) => (
          <HouseRow key={house.id} house={house} />
        ))}
      </tbody>
    </table>
  );
};

// In App.jsx: mandatory Suspense wrapper
const App = () => (
  <Suspense fallback={<div>Loading houses...</div>}>
    <HouseList />
  </Suspense>
);

use is not a hook (hook rules do not apply). It can be used conditionally.

useMemo

import { useMemo } from 'react';

const HouseList = ({ houses }) => {
  // Memoized computation: only recalculated when houses changes
  const totalValue = useMemo(() => {
    return houses.reduce((sum, house) => sum + house.price, 0);
  }, [houses]);

  return (
    <div>
      <p>Total Value: ${totalValue.toLocaleString()}</p>
    </div>
  );
};

useRef

import { useRef, useEffect } from 'react';

const SearchBox = () => {
  // 1. Direct DOM element access
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // auto focus on mount
  }, []);

  // 2. Persistent value without triggering re-render
  const renderCount = useRef(0);

  // Does NOT trigger re-render
  renderCount.current += 1;

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Search..." />
      <p>Rendered {renderCount.current} times</p>
    </div>
  );
};

useState vs useRef:

useStateuseRef
Triggers re-renderYesNo
Persistent value between rendersYesYes
Typical usageData displayed in UICounters, DOM access

6. Conditional Rendering and Shared State

Conditional Rendering

const HouseRow = ({ house }) => {
  const { address, country, price } = house;

  // Option 1: JSX variable with if/else
  let priceTd;
  if (price < 500000) {
    priceTd = <td>{price}</td>;
  } else {
    priceTd = <td className="text-primary">{price}</td>;
  }

  // Option 2: ternary operator (inline)
  return (
    <tr>
      <td>{address}</td>
      <td>{country}</td>
      <td className={price >= 500000 ? 'text-primary' : ''}>{price}</td>
    </tr>
  );
};
// Option 3: && operator (logical short-circuit)
{price && <td className="text-primary">{price}</td>}

// Option 4: early return
const LoadingOrContent = ({ isLoading, data }) => {
  if (isLoading) return <div>Loading...</div>;
  return <div>{data}</div>;
};

// Option 5: conditional component rendering
const App = () => {
  const [selectedHouse, setSelectedHouse] = useState(null);

  return (
    <>
      <Banner />
      {selectedHouse
        ? <House house={selectedHouse} />
        : <HouseList onSelect={setSelectedHouse} />
      }
    </>
  );
};

Passing Functions as Props

flowchart TD
    A["App\nstate: selectedHouse\nsetSelectedHouse()"] -->|"prop: onHouseSelect={setSelectedHouse}"| B["HouseList"]
    B -->|"prop: onHouseSelect={onHouseSelect}"| C["HouseRow"]
    C -->|"onClick: onHouseSelect(house)"| A

Mounting and Unmounting

stateDiagram-v2
    [*] --> Mounted : Component appears in the DOM
    Mounted --> Rendered : Function executed, effects launched
    Rendered --> ReRendered : State or props change
    ReRendered --> Rendered
    Rendered --> Unmounted : Component removed from DOM
    Unmounted --> [*] : State destroyed, effects cleaned up
    Unmounted --> Mounted : Component remounts (state re-initialized)

When a component is unmounted, all its state is destroyed. When it remounts, it starts from the initial state.

useCallback

import { useCallback } from 'react';

const App = () => {
  const [selectedHouse, setSelectedHouse] = useState(null);

  // Without useCallback: new reference on each re-render
  // With useCallback: same reference between re-renders (useful with React.memo)
  const selectHouse = useCallback((house) => {
    setSelectedHouse(house);
  }, []); // []: the function never changes

  return <HouseList onHouseSelect={selectHouse} />;
};

Custom Hooks

A custom hook is a function whose name starts with use and can call other hooks.

// src/hooks/useHouses.js
import { useState, useEffect } from 'react';

const useHouses = () => {
  const [houses, setHouses] = useState([]);
  const [loadingState, setLoadingState] = useState('isLoading');

  useEffect(() => {
    const fetchHouses = async () => {
      try {
        setLoadingState('isLoading');
        const response = await fetch('http://localhost:6001/houses');
        const data = await response.json();
        setHouses(data);
        setLoadingState('loaded');
      } catch (error) {
        setLoadingState('hasErrored');
      }
    };

    fetchHouses();
  }, []);

  return { houses, setHouses, loadingState };
};

export default useHouses;

Custom hook advantages:

  • Separation of concerns (fetching vs display)
  • Reusability
  • Testability
  • Each use of the hook has its own isolated state

Error Boundaries

// src/components/ErrorBoundary.jsx
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

// Usage
<ErrorBoundary fallback={<p>An error occurred in the list.</p>}>
  <HouseList />
</ErrorBoundary>

Error Boundaries do not catch errors in event handlers, asynchronous code, or Server Components.


7. Context and Navigation

Introduction to Context

The problem: Passing props through many levels of components (prop drilling).

Context makes state available to all components in the tree without prop drilling.

Mutable Context

// src/context/navigationContext.js
import { createContext } from 'react';

export const navigationContext = createContext('home');

export const navValues = {
  home: 'home',
  house: 'house',
};
// App.jsx — Provider
import { useState, useCallback } from 'react';
import { navigationContext, navValues } from './context/navigationContext';

const App = () => {
  const [nav, setNav] = useState({ page: navValues.home, param: null });

  const navigate = useCallback((page, param = null) => {
    setNav({ page, param });
  }, []);

  return (
    <navigationContext.Provider value={{ nav, navigate }}>
      <Banner />
      {nav.page === navValues.home && <HouseList />}
      {nav.page === navValues.house && <House house={nav.param} />}
    </navigationContext.Provider>
  );
};

Consuming the Context

// HouseRow.jsx
import { useContext } from 'react';
import { navigationContext, navValues } from '../context/navigationContext';

const HouseRow = ({ house }) => {
  const { navigate } = useContext(navigationContext);

  return (
    <tr onClick={() => navigate(navValues.house, house)}>
      <td>{house.address}</td>
      <td>{house.price}</td>
    </tr>
  );
};

When to Use Context

Use Context when:

  • The same state needs to be shared by many components
  • Navigation through multiple levels is needed

Disadvantages to consider:

  • Potentially many re-renders when context changes
  • Component depends on a present context → harder to reuse
  • “Hidden” state — difficult to locate without knowing the code well

React Router

For advanced needs (dedicated URLs, deep linking, browser back button):

npm install react-router
// App.jsx with React Router v7
import { BrowserRouter, Routes, Route } from 'react-router';

const App = () => (
  <BrowserRouter>
    <Banner />
    <Routes>
      <Route index element={<HouseList />} />
      <Route path="house/:id" element={<House />} />
    </Routes>
  </BrowserRouter>
);

// HouseRow.jsx — programmatic navigation
import { useNavigate } from 'react-router';

const HouseRow = ({ house }) => {
  const navigate = useNavigate();

  return (
    <tr onClick={() => navigate(`/house/${house.id}`, { state: house })}>
      <td>{house.address}</td>
    </tr>
  );
};

Use navigate() and <Link> instead of <a href> to avoid a full SPA reload.


8. User Input and Forms

Controlled Components

import { useState } from 'react';

const SearchForm = () => {
  const [firstName, setFirstName] = useState('');
  const [isSubscribed, setIsSubscribed] = useState(false);

  return (
    <div>
      <input
        type="text"
        value={firstName}  // bound to state
        onChange={(e) => setFirstName(e.target.value)}  // updates state
        placeholder="First name"
      />

      <input
        type="checkbox"
        checked={isSubscribed}
        onChange={(e) => setIsSubscribed(e.target.checked)}
      />

      <p>Hello, {firstName}!</p>
    </div>
  );
};

React Events vs HTML Events:

HTML EventReact Event (camelCase)
onclickonClick
onchangeonChange
onsubmitonSubmit
onkeydownonKeyDown
onfocusonFocus
onbluronBlur
onmouseoveronMouseOver

Forms

import { useState } from 'react';

const PersonForm = () => {
  const [person, setPerson] = useState({ firstName: '', lastName: '' });

  // Generic handler with computed property name
  const handleChange = (e) => {
    setPerson({
      ...person,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();  // Prevents page reload
    console.log('Submitted:', person);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="firstName"
        value={person.firstName}
        onChange={handleChange}
        placeholder="First name"
      />
      <input
        type="text"
        name="lastName"
        value={person.lastName}
        onChange={handleChange}
        placeholder="Last name"
      />
      <button type="submit">Submit</button>
    </form>
  );
};

Uncontrolled Components

import { useRef } from 'react';

const UncontrolledForm = () => {
  const firstNameRef = useRef(null);
  const lastNameRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(firstNameRef.current.value, lastNameRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={firstNameRef} type="text" defaultValue="John" />
      <input ref={lastNameRef} type="text" />
      {/* File input: always uncontrolled */}
      <input type="file" ref={fileRef} />
      <button type="submit">Submit</button>
    </form>
  );
};

useTransition and Actions

import { useState, useTransition } from 'react';

const BidForm = ({ houseId, onBidAdded }) => {
  const [isPending, startTransition] = useTransition();
  const [bidAmount, setBidAmount] = useState(0);

  const handleSubmit = () => {
    startTransition(async () => {
      await addBid({ houseId, amount: bidAmount });
      onBidAdded();
    });
  };

  return (
    <div>
      <input
        type="number"
        value={bidAmount}
        onChange={(e) => setBidAmount(parseInt(e.target.value))}
      />
      <button onClick={handleSubmit} disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Bid'}
      </button>
    </div>
  );
};

Form Actions (React 19)

// Simplified: no more controlled state or onChange
const BidForm = ({ houseId }) => {
  const bidAction = async (formData) => {
    const bidder = formData.get('bidder');
    const amount = parseInt(formData.get('amount'));

    await addBid({ houseId, bidder, amount });
    // Inputs are automatically reset after submission
  };

  return (
    <form action={bidAction}>
      <input type="text" name="bidder" placeholder="Your name" />
      <input type="number" name="amount" placeholder="Bid amount" />
      <button type="submit">Add Bid</button>
    </form>
  );
};

useActionState

import { useActionState } from 'react';

const BidsComponent = ({ houseId, initialBids }) => {
  const addBidAction = async (previousState, formData) => {
    const newBid = {
      id: Date.now(),
      houseId,
      bidder: formData.get('bidder'),
      amount: parseInt(formData.get('amount')),
    };

    await postBidToAPI(newBid);

    return { error: null, bids: [...previousState.bids, newBid] };
  };

  const [state, actionFn, isPending] = useActionState(
    addBidAction,
    { error: null, bids: initialBids }
  );

  return (
    <div>
      {state.error && <p className="text-danger">{state.error}</p>}
      <table>
        <tbody>
          {state.bids.map((bid) => (
            <tr key={bid.id}>
              <td>{bid.bidder}</td>
              <td>{bid.amount}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <form action={actionFn}>
        <input type="text" name="bidder" />
        <input type="number" name="amount" />
        <button type="submit" disabled={isPending}>
          {isPending ? 'Adding...' : 'Add Bid'}
        </button>
      </form>
    </div>
  );
};

useOptimistic

import { useOptimistic } from 'react';

const useBids = (houseId) => {
  const [bids, setBids] = useState([]);

  // optimisticBids: optimistic version (immediate update before API confirmation)
  const [optimisticBids, addOptimisticBid] = useOptimistic(
    bids,
    (currentBids, newBid) => [...currentBids, newBid]  // reducer
  );

  const addBid = async (bid) => {
    // 1. Immediate UI update (optimistic)
    addOptimisticBid({ ...bid, id: 'temp-' + Date.now() });
    // 2. Background API call
    const savedBid = await postBid(bid);
    // 3. UI reflects real state once API responds
    setBids((prev) => [...prev, savedBid]);
  };

  return { bids: optimisticBids, addBid };
};

9. Server-side React and Next.js

Server-side React

flowchart LR
    subgraph SPA["SPA (Vite/React pure)"]
        B1[Browser] -->|"1 request: JS bundle"| S1[Static server]
        B1 -->|"2 request: data"| API1[API]
    end

    subgraph SSR["Server-side (Next.js)"]
        B2[Browser] -->|URL Request| S2[Node.js Server]
        S2 -->|Fetch data directly| DB[(Database)]
        S2 -->|Rendered HTML| B2
    end

Server-side advantages:

  • Secrets (API keys, tokens) stay on the server
  • JS libraries aren’t sent to the browser
  • Better initial performance (data included in first HTML)
  • Facilitates SEO and caching

Next.js — Overview

npx create-next-app@15

cd my-app
npm run dev

Next.js Scripts:

CommandDescription
npm run devDevelopment server with Turbopack
npm run buildProduction build
npm startLaunches the production build

Pages, Routing and Layouts

Next.js uses the App Router based on file structure:

app/
├── layout.jsx      # Root layout (html, body, Banner)
├── page.jsx        # Route "/" → HouseList
├── house/
│   └── [id]/       # Route "/house/:id"
│       └── page.jsx → House
└── globals.css
// app/layout.jsx
import Banner from '../components/Banner';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div className="container">
          <Banner />
          {children}  {/* Content of each page */}
        </div>
      </body>
    </html>
  );
}

Server Components

By default in Next.js, all components are Server Components.

// app/house/[id]/page.jsx — Server Component
// No 'use client' → runs on the server

export default async function HousePage({ params }) {
  const { id } = await params;

  // Direct API (or database) call — no useEffect needed
  const house = await fetch(`http://localhost:6001/houses/${id}`).then(r => r.json());
  const bidsPromise = fetch(`http://localhost:6001/bids?houseId=${id}`).then(r => r.json());

  return (
    <div>
      <h2>{house.address}</h2>
      <Suspense fallback={<div>Loading bids...</div>}>
        <Bids house={house} bidsPromise={bidsPromise} />
      </Suspense>
    </div>
  );
}

Client Components

// src/components/AddHouseButton.jsx
'use client';  // Marks this component as a Client Component

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { addHouse } from '../actions/houseActions';

const AddHouseButton = () => {
  const [isLoading, setIsLoading] = useState(false);
  const router = useRouter();

  const handleClick = async () => {
    setIsLoading(true);
    await addHouse();
    router.refresh();  // Asks Next.js to refresh data
    setIsLoading(false);
  };

  return (
    <button
      className="btn btn-primary"
      onClick={handleClick}
      disabled={isLoading}
    >
      {isLoading ? 'Adding...' : 'Add House'}
    </button>
  );
};

export default AddHouseButton;

Server/Client Component Rules:

Server ComponentClient Component
Hooks (useState, useEffect)
Event handlers (onClick)
Fetch data✅ (direct DB)Via API
File system access
Can render Server Components❌ (except via children)
Sent to browserHTML onlyJS + HTML

Server Actions

// src/actions/houseActions.js
'use server';  // This entire file runs on the server

import { revalidatePath } from 'next/cache';

export const addHouse = async (formData) => {
  const newHouse = {
    address: formData?.get('address') || 'New House',
    country: formData?.get('country') || 'France',
    price: parseInt(formData?.get('price') || '0'),
  };

  await fetch('http://localhost:6001/houses', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newHouse),
  });

  // Invalidates the home page cache → next access re-renders server-side
  revalidatePath('/');
};

10. Application Design

Component Hierarchy

Single responsibility principle: Each component should do one thing.

flowchart TD
    A[Mock UI] --> B[Identify components]
    B --> C["Root Component\n(App or Page)"]
    C --> D[Banner]
    C --> E[HouseList]
    E --> F["HouseRow ×N"]
    C --> G[House]
    G --> H[BidList]
    G --> I[AddBid]

File Structure

Group by type (small projects):

src/
├── components/
│   ├── Banner.jsx
│   ├── HouseList.jsx
│   ├── HouseRow.jsx
│   ├── House.jsx
│   ├── BidList.jsx
│   ├── AddBid.jsx
│   └── ErrorBoundary.jsx
├── hooks/
│   ├── useHouses.js
│   └── useBids.js
├── context/
│   └── navigationContext.js
├── App.jsx
└── main.jsx

Group by feature (large projects):

src/
├── features/
│   ├── houses/
│   │   ├── HouseList.jsx
│   │   ├── HouseRow.jsx
│   │   ├── House.jsx
│   │   └── useHouses.js
│   └── bids/
│       ├── BidList.jsx
│       ├── AddBid.jsx
│       └── useBids.js
├── common/
│   ├── Banner.jsx
│   └── ErrorBoundary.jsx
└── App.jsx

Identifying and Placing State

Checklist: is it state?

CriterionYes → Possible stateNo → Not state
Passed via props?NoYes → Props, not state
Changes over time?YesNo → Constant
Can be computed?NoYes → Derived value

Place state as low as possible in the hierarchy:

  • newBid state → AddBid (only component that uses it)
  • bids state → House (used by BidList AND AddBid → common parent)
  • houses state → HouseList (only component that displays them directly)
  • selectedHouse state → App (controls which “page” is displayed)

Inverse Data Flow

flowchart TD
    A["App\nstate: selectedHouse"] -->|navigate prop| B[HouseList]
    B -->|navigate prop| C[HouseRow]
    C -->|"onClick: navigate(house)"| A
    style A fill:#4CAF50,color:#fff
    style C fill:#2196F3,color:#fff
// Design steps:
// 1. Build a static version (props only, no state)
// 2. Identify what should be state
// 3. Place state in the right place in the hierarchy
// 4. Identify inverse flows (child → parent)
// 5. Pass functions as props for inverse data flow

const App = () => {
  const [selectedHouse, setSelectedHouse] = useState(null);

  return selectedHouse
    ? <House house={selectedHouse} onBack={() => setSelectedHouse(null)} />
    : <HouseList onHouseSelect={setSelectedHouse} />;
};

Key Concepts Summary

mindmap
  root((React))
    Components
      Function Components
      JSX
      Props
      Children
    State
      useState
      useReducer
      Context API
    Side Effects
      useEffect
      useCallback
      useMemo
      useRef
    Forms
      Controlled Components
      Form Actions
      useActionState
      useOptimistic
    Performance
      Virtual DOM
      Reconciliation
      React.memo
      useTransition
    Tools
      Vite
      Next.js
      React Router
      ESLint

Resources


Search Terms

react · fundamentals · typescript · frontend · development · components · context · component · rendering · state · actions · hooks · prop · props · conditional · css · data · effects · forms · functions · javascript · next.js · server · server-side

Interested in this course?

Contact us to book it or get a custom training plan for your team.