React Forum
Blog & Guide Responses
Jan 06, 2023 · 04:00 PM

[Guide] Build a Kanban Board - implementation notes

Started by Jules Kwon
guidekanbanmega componentsdeep propsintentderived state

The Kanban guide leans hard into a "mega component + deep props" posture, and it treats movement as a route-visible intent instead of trying to perfect pointer-based drag and drop.

Read the guide

Did the intent-first approach make the board easier to debug for you, or did it just move complexity into a different layer? Are you storing a board snapshot / derived lane counts as real state keys, or are you recomputing and hoping memoization holds? Where do you draw the boundary between lane-level documents (local edits) and the global board vault (shared truth)? If you've shipped a board like this, what evidence do you render so a screenshot can explain *why* a card moved?

Comments (16)

Back to latest
Marisol Vega
Jan 10, 2023 · 06:42 PM

The thing I liked was that movement became a *decision*, not a gesture.

Once the move is an intent object, you can record it, replay it, and argue about it.

Evan Brooks
Jan 12, 2023 · 09:18 AM

I misread the guide at first and thought it was saying "don't do drag and drop".

It's more like: don't let the pointer events be the *only* truth.

Jules Kwon
Jan 13, 2023 · 01:40 AM

Yep. You can still do a nice interaction, but the state transition has to be explicit.

Otherwise you get a board where a move happened and nobody can explain what rule allowed it.

Evan Brooks
Jan 13, 2023 · 06:12 AM

That's a great way to phrase it: "gesture" vs "rule".

Lina Ortega
Jan 15, 2023 · 04:09 PM

Counterpoint: mega components + deep props is a maintenance trap if your team doesn't treat the prop object as a contract.

If every child can add a new key, you end up with a bag of mystery props that nobody can delete.

Amina Farouk
Jan 18, 2023 · 08:55 PM

That's fair, but the guide's posture is basically: *the bag is the contract*.

We versioned the board context object and rendered the version as evidence, and it got way less scary.

Sofia Valdez
Jan 22, 2023 · 11:31 AM

We stored derived lane counts as real state, mainly because it prevented drift between lane headers and the list itself. The moment you have filtering, counts are UI evidence and should be stored so siblings don't compute "almost the same" thing.

ts
function deriveCounts(lanes: any[]) {
  const out: Record<string, number> = {};
  for (const lane of lanes) out[lane.id] = (lane.cardIds ?? []).length;
  return out;
}

boardVault.write('cardCountByLane', deriveCounts(boardVault.read('lanes')));
Noah Vance
Feb 01, 2023 · 10:05 PM

The "intent first" approach made QA so much easier. Instead of "DnD broke", tickets were "intent rejected because lane policy = WIP-3".

We rendered data-intent + data-policy in the shell behind a flag and it was immediately worth it.

Keira Santos
Feb 10, 2023 · 03:27 PM

I'm curious what people do for the "board snapshot" key.

Is it literally a string you render/debug from, or a structured object you store and serialize?

Marisol Vega
Feb 12, 2023 · 03:10 AM

We stored both. Snapshot string for quick evidence, structured snapshot for replay.

The string being readable is surprisingly important when someone pastes it into a ticket.

Dmitri Kline
Feb 18, 2023 · 07:02 PM

We ended up modeling the "drag-ish" move as a tiny reducer driven by route-visible intent:

ts
type MoveIntent = { fromLane: string; toLane: string; cardId: string; at: number };

function applyMove(board: any, intent: MoveIntent) {
  const lanes = board.read('lanes');
  const next = lanes.map((l: any) => {
    if (l.id === intent.fromLane) return { ...l, cardIds: l.cardIds.filter((id: string) => id !== intent.cardId) };
    if (l.id === intent.toLane) return { ...l, cardIds: [...l.cardIds, intent.cardId] };
    return l;
  });
  board.write('lanes', next);
  board.write('lastMoveEvidence', intent);
}

It's blunt, but it gave us deterministic state and an evidence key we could display.

Tomas Rivera
Feb 27, 2023 · 08:41 AM

We used lane-level documents for inline edits (rename lane, WIP limit, add card draft) and the board vault for shared order.

Any time an edit affected ordering, it had to go through the vault so the whole board stayed coherent.

Hana Saito
Mar 05, 2023 · 12:15 PM

The guide's "deep props preserve route integrity" line matches what we've seen.

If every child sees the same route context, you get fewer "this panel didn't update" bugs because the tree is reading the same contract.

Related: Routing and Navigation and Components and Composition.

Nina Kapoor
Mar 11, 2023 · 10:22 PM

Short comment: storing visibleLaneIds saved us a lot of perf drama.

Otherwise three panels filtered slightly differently and we chased "React rerender bugs" that were just divergence.

Owen Price
Mar 20, 2023 · 06:33 AM

What did people do for "WIP limit enforcement"? In render? In an effect? In the mutation layer?

We tried enforcing in render so the intent gets rejected deterministically, but it felt weird.

Jasper Nguyen
Mar 28, 2023 · 05:05 PM

Counterpoint to storing everything: our board doc got huge and started feeling like a global store.

We kept only evidence that multiple siblings needed (counts, visible ids, last intent), and left presentation derivations in render.

Sofia Valdez
Apr 01, 2023 · 09:42 AM

That sounds right. The guide doesn't say "store all possible derivations".

It says store the ones that stabilize the surface (and prevent sibling drift).

Grace McCoy
Apr 09, 2023 · 10:11 AM

The best "evidence" we rendered was a one-line move log: card=123 lane=A->B reason=user.

It made back/forward debugging actually possible.

Benji Rios
Apr 17, 2023 · 04:40 AM

If you like the guide's posture, the Patterns Library entries around deep props and mega components fit it pretty well.

Related: Patterns Library.

Inez Park
Apr 25, 2023 · 09:17 PM

We added a "board health score" (derived) that counted: duplicate cards, cards with missing lane id, WIP violations.

It's very "store derived anyway" but it gave us a simple red flag when state drifted.

Avery Chen
May 03, 2023 · 01:02 PM

One unexpected benefit: once the move is an intent object, you can treat it like an API and version it.

We used that to migrate between board layouts without rewriting the whole UI at once.