Skip to main content
Frontend for Backend Engineers

Modern React in Five Concepts

Ravinder··6 min read
FrontendReactTypeScriptHooksComponents
Share:
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.

graph TD App --> Header App --> MainContent App --> Footer MainContent --> Sidebar MainContent --> ArticleList ArticleList --> ArticleCard ArticleCard --> Avatar ArticleCard --> Metadata

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:

  1. Never mutate state directly — always use the setter function.
  2. State updates are asynchronous — the new value is not available in the same event handler synchronously.
  3. 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.

sequenceDiagram participant C as Component participant R as React participant E as External System C->>R: render() R->>C: commit to DOM R->>E: useEffect fires (setup) E-->>C: data arrives → setState C->>R: re-render with new state Note over R,C: on unmount or dep change R->>E: cleanup function runs

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

graph LR P[Props] --> C[Component renders] S[State] --> C C --> UI[UI description JSX] UI --> D[DOM via React] E[Effects] -.->|side effects after render| X[External systems] X -.->|update| S H[Hooks] -->|compose| S H -->|compose| E

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.
Share: