Table of Contents
- The Power of React
- Setting Up a New Project
- Styling Components
- Hooks, Props and State
- Component Rendering and Side Effects
- Conditional Rendering and Shared State
- Context and Navigation
- User Input and Forms
- Server-side React and Next.js
- 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 Type | Behavior |
|---|---|
| Traditional application | Each interaction sends an HTTP request to the server which returns a new HTML page |
| SPA | The 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:
| Rule | HTML | JSX |
|---|---|---|
| Class attribute | class="..." | className="..." |
| Style attribute | style="color:red" | style={{ color: 'red' }} |
| Self-closing | Optional | Required: <img /> |
| Single root element | Not required | Required (or <>...</>) |
| JavaScript expressions | N/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
| Command | Description |
|---|---|
npm run dev | Launches the development server with HMR |
npm run build | Generates the production build in /dist |
npm run lint | Checks the code with ESLint |
npm run preview | Tests 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.cssextension 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
styleinline 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"]
| Hook | Description |
|---|---|
useState | Manages local state in a component |
useEffect | Executes side effects after rendering |
useContext | Accesses a Context |
useRef | Stores a persistent value without triggering re-render |
useMemo | Memoizes the result of an expensive computation |
useCallback | Memoizes a function to avoid re-renders |
useReducer | Alternative to useState for complex logic |
useTransition | Marks a state update as non-urgent |
useActionState | Manages state linked to a form action (React 19) |
useOptimistic | Optimistic 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
setfunction) - 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>
);
useis 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:
useState | useRef | |
|---|---|---|
| Triggers re-render | Yes | No |
| Persistent value between renders | Yes | Yes |
| Typical usage | Data displayed in UI | Counters, 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 Event | React Event (camelCase) |
|---|---|
onclick | onClick |
onchange | onChange |
onsubmit | onSubmit |
onkeydown | onKeyDown |
onfocus | onFocus |
onblur | onBlur |
onmouseover | onMouseOver |
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:
| Command | Description |
|---|---|
npm run dev | Development server with Turbopack |
npm run build | Production build |
npm start | Launches 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 Component | Client 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 browser | HTML only | JS + 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?
| Criterion | Yes → Possible state | No → Not state |
|---|---|---|
| Passed via props? | No | Yes → Props, not state |
| Changes over time? | Yes | No → Constant |
| Can be computed? | No | Yes → Derived value |
Place state as low as possible in the hierarchy:
newBidstate →AddBid(only component that uses it)bidsstate →House(used byBidListANDAddBid→ common parent)housesstate →HouseList(only component that displays them directly)selectedHousestate →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
- Course GitHub: github.com/RolandGuijt/ps-react-fundamentals
- React Documentation: react.dev
- Vite: vitejs.dev
- Next.js: nextjs.org
- React Router: reactrouter.com
- Bootstrap: getbootstrap.com
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