Modern React in Five Concepts
React's ecosystem is enormous and the documentation surface is sprawling. If you try to learn everything at once you will drown. The good news: five concepts cover 90% of what you will read and write in a real codebase. Everything else is specialization built on top of these five.
Think of this as the minimum viable React API — the equivalent of knowing SELECT, INSERT, UPDATE, DELETE, and JOIN before worrying about window functions and CTEs.
Concept 1: Components
A component is a function that accepts an object of inputs and returns a description of UI. That's it.
// A component is just a function
function Greeting({ name }: { name: string }) {
return <p>Hello, {name}!</p>;
}
// JSX compiles to React.createElement calls
// <Greeting name="Priya" /> → React.createElement(Greeting, { name: "Priya" })Components compose. You build complex UIs by nesting small, focused components — the same way you build complex systems by composing small, focused services. The tree structure is your UI hierarchy.
Rule of thumb: if a component exceeds 80–100 lines or does more than one conceptual thing, split it.
Concept 2: Props
Props are the inputs to a component. They flow one direction — from parent to child. They are read-only inside the component that receives them.
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick: () => void;
}
function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
}TypeScript interfaces for props are the equivalent of strongly-typed function signatures — they document the contract and catch caller mistakes at compile time. Always type your props.
A special prop: children. It lets you pass JSX between the opening and closing tags of a component, enabling slot-based composition patterns.
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Usage
<Card title="Summary">
<p>Any JSX can go here.</p>
</Card>Concept 3: State
State is mutable data that, when changed, triggers a re-render of the component and its descendants.
import { useState } from "react";
function Counter() {
// [currentValue, setterFunction] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
);
}Critical rules for state:
- Never mutate state directly — always use the setter function.
- State updates are asynchronous — the new value is not available in the same event handler synchronously.
- When new state depends on the previous state, use the functional update form:
setCount(c => c + 1).
For complex state with multiple related fields, useReducer is the cleaner choice — it mirrors the reducer pattern you know from event-sourced systems.
type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "reset": return 0;
}
}
const [count, dispatch] = useReducer(reducer, 0);
dispatch({ type: "increment" });Concept 4: Effects
Effects let you synchronize a component with something outside React — an API, a browser API, a WebSocket, a timer.
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// This runs after the component renders
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) setUser(data);
});
// Cleanup runs before the effect re-fires or component unmounts
return () => { cancelled = true; };
}, [userId]); // Dependency array: re-run when userId changes
return user ? <div>{user.name}</div> : <div>Loading…</div>;
}The dependency array is the most misunderstood part. Think of it as a cache invalidation key: the effect re-runs whenever any dependency changes. An empty array [] means "run once on mount" — equivalent to a constructor or @PostConstruct lifecycle method.
Concept 5: Hooks
Hooks are functions that start with use and let you compose stateful logic. The built-in hooks (useState, useEffect, useContext, useRef, useMemo, useCallback) are primitives. Custom hooks let you extract and reuse that logic across components.
// Custom hook: encapsulates fetch logic, reusable anywhere
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// Consumer is clean and focused on rendering
function UserProfile({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user!.name}</div>;
}This pattern is the frontend equivalent of a service layer — the hook is your service, the component is your controller. Hooks must only be called at the top level of a component or another hook, never inside loops, conditions, or callbacks.
How the Five Connect
Props and state feed the render. Effects synchronize with the outside world and feed back into state. Hooks are the composition primitive for all of it.
Key Takeaways
- Components are pure functions from props + state to JSX — think of them as typed, composable view functions.
- Props are immutable inputs flowing downward; type them with TypeScript interfaces for enforced contracts.
- State triggers re-renders — never mutate it directly, and use functional updates when depending on the previous value.
- Effects are lifecycle hooks for external synchronization — always include a cleanup function and model the dependency array as a cache key.
- Hooks are the composition primitive — extract reusable stateful logic into custom
use*functions, separating data concerns from rendering. - The service/controller split maps cleanly: custom hooks own data and side effects; components own rendering.