· 4 min read

React Core Concepts: A Practical, No‑Fluff Guide

Components, JSX, props, state, effects, hooks, context, reconciliation, keys, and patterns — the mental model you actually use when building React apps.

Why React exists

React’s job is simple: render UI from state, then keep the DOM in sync when state changes. You describe “what the UI should look like for a given state,” and React handles updates efficiently.

The two most important ideas:

  • UI = f(state)
  • Component identity (keys) determines where state lives across renders

Components and JSX

Components are pure(ish) functions of props and state.

function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}

JSX is just syntax sugar for React.createElement. Keep render logic pure: no side effects, no subscriptions, no imperative DOM writes here.

Props vs State

  • Props: external, read-only inputs.
  • State: internal, owned by the component; changing state triggers a re-render.
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>Clicked {count} times</button>;
}

Avoid duplicating derived state. Compute when you can.

function Cart({ items }) {
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  return <div>Total: ${total.toFixed(2)}</div>;
}

Lists and keys (component identity)

Keys tell React how to match old vs new elements during reconciliation.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id}>{t.text}</li>
      ))}
    </ul>
  );
}

Never use array index as a key for dynamic lists. Wrong keys cause state to “move” between items.

Events and controlled inputs

Use controlled components for predictable forms.

function NameField() {
  const [name, setName] = useState('');
  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}

Effects: side effects and lifecycle

Effects synchronize the component with systems outside render (network, DOM APIs, subscriptions). They run after paint by default.

Rules of thumb:

  • If you can do it during render, don’t use an effect.
  • Use the dependency array precisely; every value used inside must be declared, or intentionally stable via refs/memos.
  • Cleanup subscriptions in the return function.
function OnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const on = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener('online', on);
    window.addEventListener('offline', off);
    return () => {
      window.removeEventListener('online', on);
      window.removeEventListener('offline', off);
    };
  }, []);

  return <span>{online ? 'Online' : 'Offline'}</span>;
}

Memoization: useMemo, useCallback, React.memo

Only memoize to avoid real performance issues; premature memoization adds complexity.

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return items.map((i) => <div key={i.id}>{i.label}</div>);
});

function Parent({ items }) {
  const stableItems = useMemo(() => items.filter(Boolean), [items]);
  const handleClick = useCallback(() => {
    /* do something */
  }, []);
  return <ExpensiveList items={stableItems} onClick={handleClick} />;
}

Refs: stable boxes and DOM access

useRef gives you a mutable container that doesn’t cause re-renders.

function FocusOnMount() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  return <input ref={inputRef} />;
}

Use refs for imperative escape hatches (DOM, external libraries), not for render data.

Context: pass data without drilling

Good for theme, auth, config. Split contexts to reduce needless re-renders.

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div data-theme={theme}>...</div>;
}

Reconciliation and rendering model

React diffing preserves component state by element position and key. If you change keys or structure, you change identity and reset state. Keep state close to where it’s used (“state colocation”), lift state up only when multiple descendants need to coordinate.

Concurrent rendering and transitions (React 18+)

Concurrent rendering lets React pause, resume, and abort renders. Use transitions to mark non-urgent updates so React keeps the UI responsive.

import { startTransition, useState } from 'react';

function Search({ queryDataSource }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function onChange(e) {
    const next = e.target.value;
    setQuery(next);
    startTransition(() => {
      setResults(queryDataSource.search(next));
    });
  }

  return (
    <>
      <input value={query} onChange={onChange} />
      <ResultsList results={results} />
    </>
  );
}

Suspense lets you declaratively handle loading states for async boundaries. Combine with transitions for smooth UX.

Patterns that scale

  • Prefer composition over inheritance
  • Co-locate state with the component that owns it
  • Derive data instead of duplicating state
  • Keep effects minimal and focused on I/O
  • Use stable keys and avoid index keys in dynamic lists
  • Memoize only when measurements say you should

Common footguns

  • Missing dependency in an effect → stale values, bugs
  • Using array index as key → state jumps between items
  • Mixing controlled and uncontrolled inputs → inconsistent UI
  • Keeping redundant state → drift and extra bugs

TL;DR mental model

  • Describe UI as a function of state.
  • Manage identity with keys.
  • Side effects live in effects; keep them small and precise.
  • Optimize with memoization only when necessary.
  • Share data with context, but split contexts to avoid over-rendering.

Sources: https://youtu.be/wIyHSOugGGw

Back to Blog