DEV Community

ICraftCode
ICraftCode

Posted on

Thinking Declaratively: Why Your React Components Should Be "Mirrors"

In React, there are two ways to solve any problem: you can act like a Chef (Imperative) or you can act like a Customer (Declarative).

1. The Core Philosophy: "What" vs. "How"

Imperative (How): You provide step-by-step instructions. "Open the fridge, take two eggs, crack them into the pan, turn the heat to medium, flip after 2 minutes." If you miss a single step, the kitchen catches fire.

Declarative (What): You describe the end state. "I'd like two fried eggs, please." You don't care how the stove works or which spatula is used; you just describe what should be on the plate.

2. A Simple Example: Filtering an Array

Imagine you have a list of numbers. You want only the numbers greater than 2.
The Imperative Way (Giving Instructions)
You manually manage a new array and tell the computer exactly how to fill it.

const numbers =;
const filtered = [];

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] > 2) {
    filtered.push(numbers[i]); // "Do this, then do that"
  }
}

Enter fullscreen mode Exit fullscreen mode

You are responsible for the loop index, the temporary state, and the logic. It's "noisy" code.

The Declarative Way (Describing the Result)
You describe the relationship between the input and the output.

const numbers =;

const filtered = numbers.filter(n => n > 2); // "Give me numbers > 2"

Enter fullscreen mode Exit fullscreen mode

The Beauty: You describe the result you want. JavaScript handles the process.

3. The "Secret Truth": Abstraction

A common question arises: "But isn't someone still doing the manual work?"
Yes. Under the hood, the JavaScript Engine (V8) is running a for loop for that .filter(). When you render a React component, React's Reconciliation Engine is manually calling DOM APIs to update the screen.
Declarative programming is an abstraction built on top of imperative code. We outsource the "How" to the experts (the language and framework authors) so we can focus entirely on the "What" (our product logic).

4. The Real-World Chaos: Deleting a Role
The Imperative Way (The "Instruction" Nightmare)
Imagine you just clicked "Delete" on a Role. In our old imperative code, we had to manage a complex chain of events:

The Parent starts the deletion.
The Table (the "Chef") is holding its own internal copy of the data.
The Conflict: Even though the Role is deleted on the server, it still stays on the screen because the Table’s internal useState hasn't been "told" to remove it yet.
The Fix (More Chaos): We then have to write a manual instruction: "Hey Table, I know you have your own state, but please run this filter function to remove ID #123."
The Result: You end up with "Zombie Rows"—items that are dead in the database but alive on your screen because the manual "sync" instruction failed or was delayed.
Code smell: We were writing useEffect blocks that looked like this:

useEffect(() => {
  // Manual instruction: "If props change, force my internal state to match"
  setLocalEdges(props.edges); 
}, [props.edges]); 
Enter fullscreen mode Exit fullscreen mode

This is where the Infinite Re-render lives. If setLocalEdges accidentally triggers a parent update, the loop never ends.

✅ The Declarative Way (The "Mirror" Solution)
In the refactored version, we removed the Table's "brain" (internal state). It became a Mirror.
The Parent simply keeps a list of deletedRoleIds.
The Rule: The Parent says: "If an ID is in this 'Deleted' list, it doesn't exist."
The Render: The Table just follows the rule.

// The Parent's "Rule"
const renderRow = (role) => {
  if (deletedRoleIds.has(role.id)) return null; // Declarative: "Deleted = Nothing"
  return <RoleRow data={role} />;
};


// The Table's "Reflection"
const visibleRows = edges.map(renderRow).filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

Why this solved our real-world problem:
No more "Zombie Rows": The moment the Parent adds an ID to the deletedRoleIds Set, the row disappears. There is no "syncing" needed **because the Table is just a direct **reflection of the Parent's truth.
Zero Sync Logic: We deleted the useEffect entirely. By Lifting State Up, we moved from a fragile "syncing" process to a Single Source of Truth.
Performance: React's Reconciliation Engine is built for this. It handles the "how" of removing that row from the DOM much faster than our manual useEffect logic ever could.

Top comments (0)