Skip to solution
easyFrontend

Explain the concept of 'unidirectional data flow' in React.

792 views
01

Understand the problem

Question presented to candidate: "React is often described as having unidirectional data flow. What does that actually mean, and why does it matter?"

What a strong answer should cover:

  • Data moves one way — from parent to child through props. A child never writes to its parent state directly.
  • The only upward path is invoking a callback the parent passed down. Summed up as "props down, events up".
  • Props are read-only. Mutating one is a contract violation; React freezes props in development so the assignment throws.
  • Why it matters: for any wrong value on screen there is exactly one owner, so debugging is a walk up the tree rather than a search of everything that could have written to it.
  • The contrast is two-way binding (Angular's ngModel, Vue's v-model), where a child can write straight back into a parent's value — less boilerplate, but the write can come from anywhere.
  • Controlled components are the same rule applied to form inputs: value down, change event up.
  • It does not mean data can only travel down the tree — Context and stores still exist. Those change where the value lives, not the direction it flows from its owner.
  • Consequence: shared state gets lifted to a common ancestor.

Clarifying questions expected:

  • "Do you want the principle, or how it plays out in forms and shared state?"

Code / implementation expected: Optional. A child calling a parent's callback demonstrates it in a few lines.

data flowstate managementarchitectureprops
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Anyone preparing for a React interview — assumes props and state. Difficulty: Easy

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The prop-mutation result in section 4 was produced by actually assigning to a prop on

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Props down, events up — and what happens when you try to mutate
Run Playground
import { useState } from "react";

// A child that only READS its props and asks the parent to change things.
// It owns nothing; it cannot corrupt anything above it.
function TodoItem({ todo, onToggle, onRename }) {
  const [draft, setDraft] = useState(null);

  return (
    <li style={{ padding: "4px 0" }}>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}    // ask, do not write
      />{" "}
      {draft === null ? (
        <>
          <span style={{ textDecoration: todo.done ? "line-through" : "none" }}>{todo.text}</span>{" "}
          <button onClick={() => setDraft(todo.text)}>rename</button>
        </>
      ) : (
        <>
          <input value={draft} onChange={(e) => setDraft(e.target.value)} />{" "}
          <button onClick={() => { onRename(todo.id, draft); setDraft(null); }}>save</button>
        </>
      )}
    </li>
  );
}

// A child that tries to write to its props directly. React freezes props in
// development, so this throws rather than silently corrupting the parent.
function Rebel({ todo }) {
  const [report, setReport] = useState("not tried yet");
  const attempt = () => {
    try {
      todo.text = "I changed it myself";
      setReport("mutation succeeded — and the parent would not re-render");
    } catch (e) {
      setReport("threw: " + e.message);
    }
  };
  return (
    <p style={{ fontSize: 13 }}>
      <button onClick={attempt}>try to mutate a prop</button>{" "}
      <span style={{ color: report.startsWith("threw") ? "#161" : "#a33" }}>{report}</span>
    </p>
  );
}

export default function App() {
  // The parent OWNS the state. It is the single source of truth.
  const [todos, setTodos] = useState([
    { id: 1, text: "Read the docs", done: true },
    { id: 2, text: "Build something", done: false },
  ]);

  // The only ways the state changes — both live here, with the owner.
  const toggle = (id) =>
    setTodos((ts) => ts.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
  const rename = (id, text) =>
    setTodos((ts) => ts.map((t) => (t.id === id ? { ...t, text } : t)));

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <ul style={{ listStyle: "none", padding: 0 }}>
        {todos.map((t) => (
          <TodoItem key={t.id} todo={t} onToggle={toggle} onRename={rename} />
        ))}
      </ul>
      <Rebel todo={todos[0]} />
      <p style={{ color: "#666", fontSize: 13 }}>
        Every change to this list happens in one place — the parent. The children
        read props and call callbacks; note that both update functions return new
        objects rather than mutating, so React can see the change by reference.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 12 of 119 decoded in the React.js track. One more won't hurt.

Back to track