Back to blog
Frontend Engineeringbeginner

React Core Fundamentals: JSX, Components, State & Props

Master React's foundational concepts — JSX, Virtual DOM, class vs functional components, props, state, and lifecycle — with real-world examples from production apps.

LearnixoApril 13, 20268 min read
ReactJSXComponentsStatePropsVirtual DOM
Share:𝕏

What React Actually Is

React is a UI library — not a framework. It renders a tree of components and re-renders only what changed. That's the whole contract.

Everything else — routing, state management, data fetching — you choose separately. This is React's biggest strength and most common source of confusion for newcomers.

JSX: JavaScript + HTML, Explained Properly

JSX looks like HTML but compiles to JavaScript function calls.

JSX
// What you write
const button = <button className="btn" onClick={handleClick}>Save</button>;

// What Babel compiles it to
const button = React.createElement(
  "button",
  { className: "btn", onClick: handleClick },
  "Save"
);

Key JSX rules that trip people up:

JSX
// 1. className, not class
<div className="container">

// 2. Expressions in {}, not {{ }}
<span>{user.name}</span>
<span>{isAdmin ? "Admin" : "User"}</span>

// 3. Self-closing tags are mandatory
<img src={logo} alt="Logo" />
<br />

// 4. Single root element (or Fragment)
return (
  <>
    <Header />
    <Main />
  </>
);

// 5. camelCase event handlers
<input onChange={handleChange} onKeyDown={handleKey} />

Real-World JSX: Dashboard Card Component

JSX
function MetricCard({ title, value, trend, icon: Icon }) {
  const isPositive = trend > 0;

  return (
    <div className={`card ${isPositive ? "card--positive" : "card--negative"}`}>
      <div className="card__header">
        <Icon size={20} />
        <span className="card__title">{title}</span>
      </div>
      <div className="card__value">{value}</div>
      <div className="card__trend">
        {isPositive ? "" : ""} {Math.abs(trend)}%
      </div>
    </div>
  );
}

// Usage
<MetricCard
  title="Monthly Revenue"
  value="$42,800"
  trend={12.5}
  icon={DollarSign}
/>

The Virtual DOM: Why React Is Fast

React keeps a virtual copy of the DOM in memory. When state changes:

  1. React creates a new virtual DOM tree
  2. Diffs it against the previous tree (reconciliation)
  3. Computes the minimum set of real DOM changes
  4. Applies only those changes (commit phase)
State Change
     ↓
New Virtual DOM Tree
     ↓
Diff with Previous Virtual DOM  ← This is cheap (pure JS objects)
     ↓
Minimal Real DOM Updates        ← This is expensive (avoided when possible)

Why this matters in practice:

JSX
// Without React: browser updates the entire list on every keystroke
document.getElementById("list").innerHTML = items.map(renderItem).join("");

// With React: only changed items re-render
function ItemList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>  // key helps React identify changes
      ))}
    </ul>
  );
}

The key prop is how React matches elements between renders. Use stable, unique IDs — never array indices for dynamic lists.

Functional vs Class Components

Use functional components. Class components are legacy. Here's why:

JSX
// Class component (legacy, avoid for new code)
class UserGreeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = { greeting: "Hello" };
  }

  componentDidMount() {
    // fetch data here
  }

  render() {
    return (
      <h1>
        {this.state.greeting}, {this.props.name}!
      </h1>
    );
  }
}

// Functional component (modern, preferred)
function UserGreeting({ name }) {
  const [greeting, setGreeting] = useState("Hello");

  useEffect(() => {
    // fetch data here
  }, []);

  return <h1>{greeting}, {name}!</h1>;
}

Functional components are:

  • Shorter and easier to read
  • Easier to test
  • Better for performance optimizations
  • The only way to use hooks

Props: The Component API

Props are how parent components talk to children. Treat them as read-only.

JSX
// Define clear prop types in TypeScript
interface UserCardProps {
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  avatar?: string;          // Optional
  onDelete: (id: string) => void;  // Callback prop
}

function UserCard({ name, email, role, avatar, onDelete }: UserCardProps) {
  return (
    <div className="user-card">
      <img
        src={avatar ?? "/default-avatar.png"}
        alt={`${name}'s avatar`}
      />
      <div className="user-card__info">
        <h3>{name}</h3>
        <p>{email}</p>
        <span className={`badge badge--${role}`}>{role}</span>
      </div>
      <button onClick={() => onDelete(email)}>Remove</button>
    </div>
  );
}

Prop Patterns You'll Use Daily

JSX
// 1. Children prop — composable components
function Card({ children, className = "" }) {
  return (
    <div className={`card ${className}`}>
      {children}
    </div>
  );
}

// 2. Spread props — forwarding attributes
function Input({ label, error, ...inputProps }) {
  return (
    <div>
      <label>{label}</label>
      <input {...inputProps} className={error ? "input--error" : ""} />
      {error && <span className="error-text">{error}</span>}
    </div>
  );
}

// 3. Render prop — inversion of control
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((r) => r.json())
      .then((data) => { setData(data); setLoading(false); });
  }, [url]);

  return render({ data, loading });
}

// Usage
<DataFetcher
  url="/api/users"
  render={({ data, loading }) =>
    loading ? <Spinner /> : <UserList users={data} />
  }
/>

State: Component Memory

State is data that, when changed, causes the component to re-render.

JSX
// Correct: updating state immutably
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Learn React", done: false },
    { id: 2, text: "Build something", done: false },
  ]);

  const toggleTodo = (id) => {
    // Never mutate state directly
    // BAD: todos[0].done = true;

    // GOOD: create new array with updated item
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  };

  const addTodo = (text) => {
    setTodos((prev) => [
      ...prev,
      { id: Date.now(), text, done: false },
    ]);
  };

  const deleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  return (
    <div>
      <AddTodoForm onAdd={addTodo} />
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={toggleTodo}
          onDelete={deleteTodo}
        />
      ))}
    </div>
  );
}

State Batching (React 18+)

React 18 batches all state updates automatically, even in async code:

JSX
function handleSave() {
  // React 18: these three updates cause ONE re-render
  setName("Alice");
  setAge(30);
  setLoading(false);

  // In async functions — also batched in React 18
  setTimeout(() => {
    setCount((c) => c + 1);   // React 18: still batched
    setActive(true);           // ONE re-render
  }, 1000);
}

When to Use Local State vs Lifted State

JSX
// Local state: only this component needs it
function SearchInput() {
  const [query, setQuery] = useState("");
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

// Lifted state: multiple siblings need the same data
function FilterableTable() {
  const [filter, setFilter] = useState("");  // ← lifted here

  return (
    <>
      <FilterInput value={filter} onChange={setFilter} />
      <DataTable data={data} filter={filter} />
    </>
  );
}

Component Lifecycle (in Hooks)

The mental model for functional component lifecycle:

Mount   →  Render  →  Commit to DOM  →  Run effects
Update  →  Render  →  Commit to DOM  →  Cleanup old effects → Run new effects
Unmount →  Cleanup effects
JSX
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // componentDidMount + componentDidUpdate equivalent
  useEffect(() => {
    let cancelled = false;

    async function loadUser() {
      const data = await fetchUser(userId);
      if (!cancelled) setUser(data);  // prevent setting state after unmount
    }

    loadUser();

    // componentWillUnmount equivalent
    return () => {
      cancelled = true;
    };
  }, [userId]);  // re-runs when userId changes

  if (!user) return <Skeleton />;
  return <Profile user={user} />;
}

Conditional Rendering Patterns

JSX
function Notification({ type, message, count }) {
  // 1. Early return (cleaner for loading/error states)
  if (!message) return null;

  // 2. Ternary for binary states
  const icon = type === "error" ? <ErrorIcon /> : <InfoIcon />;

  // 3. && for optional elements (watch the falsy 0 trap!)
  // BAD: count && <Badge count={count} />  — renders "0" when count is 0
  // GOOD:
  const badge = count > 0 && <Badge count={count} />;

  // 4. Object map for multiple conditions
  const colorMap = {
    success: "green",
    warning: "orange",
    error: "red",
    info: "blue",
  };

  return (
    <div className={`notification notification--${type}`} style={{ borderColor: colorMap[type] }}>
      {icon}
      <span>{message}</span>
      {badge}
    </div>
  );
}

Lists and Keys

JSX
// Real-world: a data table with sorting and selection
function DataTable({ rows, columns }) {
  const [selected, setSelected] = useState(new Set());

  const toggleRow = (id) => {
    setSelected((prev) => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  };

  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th key={col.key}>{col.label}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rows.map((row) => (
          <tr
            key={row.id}               // always unique, stable ID
            className={selected.has(row.id) ? "row--selected" : ""}
            onClick={() => toggleRow(row.id)}
          >
            {columns.map((col) => (
              <td key={col.key}>{row[col.key]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Error Boundaries

Error boundaries catch JavaScript errors in any component below them in the tree.

JSX
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

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

  componentDidCatch(error, info) {
    // Log to error tracking (Sentry, Datadog, etc.)
    errorTracker.capture(error, {
      componentStack: info.componentStack,
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try Again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap route sections, not individual components
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <ErrorBoundary fallback={<DashboardError />}>
        <Dashboard />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
}

Interview Questions Answered

Q: What is the difference between state and props?

Props are inputs passed from parent to child — immutable from the child's perspective. State is internal data managed by the component itself that can change over time, triggering re-renders.

Q: Why must React state updates be immutable?

React compares previous and next state by reference (===). If you mutate an object directly, the reference stays the same, React sees no change, and skips the re-render. Always return new objects/arrays.

Q: When does a component re-render?

A component re-renders when: (1) its own state changes, (2) its parent re-renders (and it's not memoized), (3) a context it consumes changes.

Q: What is the key prop for?

key is React's way to identify which elements changed between renders in a list. Stable, unique keys help React reuse DOM nodes efficiently rather than destroying and recreating them. Never use array index as key for lists that can reorder or filter.

Enjoyed this article?

Explore the Frontend Engineering learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.