Building Dynamic Seat Layout Rendering in React from Sparse JSON (BookMyShow/District-style)

18/12/2025 •332 views •8 min read


Seat-selection UIs (BookMyShow, District, Ticketmaster, etc.) look straightforward: show rows, show seats, let me click one.

But theatre layouts in the real world are messy:

  • Seats are missing because there are aisles / stairs / empty spaces
  • Some rows are shorter, some are split into blocks
  • Different sections have different prices (“areas”)
  • And the backend doesn’t hand you a clean 2D matrix - it’s usually sparse data

This post is basically a walkthrough of how I took a District API seat-layout response and turned it into a UI that can render any layout dynamically (without hardcoding positions).

This blog focuses on data structure + layout strategy. For curious ones, here's the demo link


What the backend JSON looks like (District API response)

The JSON I used came from a District API response. The file is an array of layout objects, and each object contains a seatLayout.

The important nested path is:

  • seatLayout.colAreas.objArea[] → pricing sections (“areas”)
  • objArea.objRow[] → rows
  • objRow.objSeat[] → seats

Here’s a minimal representative snippet (one layout → one area → one row → two seats):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 [ { "seatLayout": { "name": "PVR", "colAreas": { "objArea": [ { "AreaNum": 1, "AreaDesc": "Recliners", "AreaCode": "RE", "HasCurrentOrder": true, "AreaPrice": 385, "objRow": [ { "GridRowId": 1, "PhyRowId": "P", "objSeat": [ { "GridSeatNum": 4, "SeatStatus": "1", "seatNumber": 1, "displaySeatNumber": "1" }, { "GridSeatNum": 12, "SeatStatus": "0", "seatNumber": 5, "displaySeatNumber": "5" } ] } ] } ] } } } ]

The detail that matters most for layout: GridSeatNum can jump (4 → 12). That jump is literally your aisle/gap. The backend doesn’t send “empty seats” for 5..11 - it just doesn’t include them.

What each field means for UI

  • AreaDesc / AreaPrice: what you show above a section (like “Recliners ₹385”)
  • PhyRowId: the row label you show on the left (“A”, “B”, “P”, “N”…)
  • GridRowId: row position (vertical placement)
  • GridSeatNum: seat position within the row (horizontal placement)
  • SeatStatus:
    • "0" → available
    • "1" → occupied/unavailable
    • everything else (for this dataset) → treat as gap


The core problem: sparse lists don’t render like a theatre

If you render seats like this:

  • row.objSeat.map(seat => <Seat />)

you’ll get something that technically shows seats… but it won’t look like the theatre map.

Because the UI needs to reflect things like:

  • empty space between seat blocks
  • aisles
  • rows that are split in the middle
  • missing rows (walking space)

A plain .map() compresses everything. The gaps disappear.

So the problem becomes:

How do you keep the “holes” in the data so the UI preserves spacing?


The strategy: indexed placement into pre-sized arrays

This is the entire trick.

Instead of rendering the sparse list directly, I convert it into something that behaves like a grid:

  1. Create an array of length X filled with undefined
  2. For every real item, place it at the index that represents its position

Then when you render the array, the missing indices stay missing - and that’s how you get gaps.

so your array would look something like [undefined, undefined, {...}, {...}, undefined]

Why this works

  • Missing indices stay undefined → that becomes a gap
  • Rendering is stable because index == coordinate
  • A “gap cell” can be rendered as an invisible placeholder with the same size as a seat

Building a “grid” for the UI

There are two places where this idea shows up:

Row-level grid (vertical positioning)

Rows come as a list, but the GridRowId gives you a placement order/position. Sometimes there are intentional skips.

So you can build an array of rows where:

  • rows[rowIndex] = row
  • missing row indices remain undefined

Later, when rendering:

  • undefined row → render a spacer row (or just keep the vertical gap)

That’s how you get gaps between rows without writing a bunch of layout-specific CSS.

Seat-level grid (horizontal positioning)

Same idea inside a row.

Seats come as a list, but each seat has GridSeatNum (think “column index”).

So you build:

  • seats[colIndex] = seat
  • missing indices remain undefined → aisle/gap

When you render the seats array:

  • real seat → render a button
  • missing seat → render a placeholder cell

Rendering becomes trivial after grid alignment

Once the data is “grid-aligned”, rendering is boring (which is what you want):

  • render areas (pricing sections)
  • for each area, render rows (including empty slots)
  • for each row, render seats (including empty slots)

The only important UI rule is:

A gap must still occupy space.

So even if a seat slot is missing, the placeholder should have the same width/height as a real seat cell. That keeps everything aligned.


Flexibility: any seat at any 2D point

At this point every seat effectively has a coordinate:

  • row index → Y
  • column index → X

And because the backend is giving you these indices (GridRowId, GridSeatNum), you’re not guessing. You’re just placing items where the backend intended.

Practical notes / lessons learned

  • Stable sizing matters

If a seat cell is size-9, then your “gap cell” also needs to be size-9. Otherwise alignment breaks.

  • Don’t let layout collapse

If you rely purely on flex without placeholders, the browser will naturally collapse empty space. The whole point of the indexed placement is to preserve those empty slots.

  • Keys and rerenders

If you generate random keys on every render, React can’t reconcile properly and you’ll get unnecessary rerenders.

For production, prefer stable keys like:

  • area.id
  • row.id
  • seat.id

Summary

If I had to summarize the whole approach:

  • The API gives you sparse lists + placement indices (GridRowId, GridSeatNum)
  • Convert sparse lists into index-addressable arrays
  • Render those arrays, and treat missing slots as real “gap cells”

That’s it. No magic. Just respecting the indices and making sure “missing” still takes up space.


If you’re reading this alongside the code:

  • one step converts the District response into internal Area/Row/Seat objects
  • another step grid-aligns rows and seats into index-based arrays
  • the UI renders those arrays, and missing indices become visible spacing

Optional future improvement:

  • If seat selection/state management grows complex, add Redux-style normalization (entities-by-id + selectors). That optimizes updates/lookups - it’s separate from the grid geometry problem

References

NOTE: LLM was used for grammatically fixes.



Found this blog helpful? Share it with your friends!