Skip to the solution
EasyFrontend6 min read

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

792 views

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 flow
  • state management
  • architecture
  • props

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactProps down, events up β€” and what happens when you try to mutate
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 12 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextEasy

What is JSX?

Continue