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

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

1) 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):

[
  {
    "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 (412). 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

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

If you render seats like this:

  • row.objSeat.map(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?

3) 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.

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

4) Building a “grid” for the UI

There are two places where this idea shows up:

4.1 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.

4.2 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

5) 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.

6) 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.

One quick note because the word “normalization” is overloaded:

Note: This is not Redux-style normalization (entities-by-id like seatsById, rowsById, etc.).

This is a grid alignment step for correct rendering geometry (gaps/aisles/spacing).

Redux normalization is great for state updates and selectors — but it doesn’t solve layout geometry on its own.

7) Handling multiple layouts in one JSON dump

In my project, the JSON can contain multiple layouts (it’s an array at the top level).

So instead of returning a single Area[], the builder returns:

  • Area[][]

    • outer array = layouts
    • inner array = that layout’s areas

Then the UI can render them in a responsive grid:

  • small screens: one below the other
  • wide screens: two columns (layout1 layout2, then layout3 layout4, …)

8) Practical notes / lessons learned

8.1 Stable sizing matters

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

8.2 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.

8.3 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
  • plus layoutIndex if you’re rendering multiple layouts

9) 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.
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Building a “Legal Killer”: An AI Agent Architecture Without the Margin for Error

Related Posts
鸿蒙next应用国际化:时间与日期格式化

鸿蒙Next应用国际化:时间与日期格式化

本文旨在深入探讨华为鸿蒙HarmonyOS Next系统(截止目前API12)在应用国际化中时间与日期格式化方面的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。 在全球化的应用场景中,正确处理时间与日期的格式化是提供优质用户体验的关键因素之一。不同地区和语言对于时间与日期的表示方式存在显著差异,鸿蒙Next系统提供了丰富的功能来满足这种多样化的需求。本文将详细介绍时间日期格式化选项、相对时间格式化、时间段格式化,以及常见时间日期格式化问题及解决方案,抛砖引玉。 一、时间日期格式化选项 (一)日期显示格式(dateStyle) 格式取值与示例 full:显示完整的日期信息,包括年、月、日、星期。例如,在中文环境下可能显示为“2023年10月15日 星期日”。 long:显示较为详细的日期,通常包含年、月、日和星期的缩写。如“2023年10月15日 周日”。 medium:显示适中的日期格式,一般有年、月、日。例如“2023-10-15”。 short:显示简洁的日期,可能只包含月、日和年的部分信息。比如“10/15/23”(在某些地区格式)。 根据区域和语言选择格式 开发者可以使用 DateTimeFormat 类,根据用户所在区域的语言和文化习惯选择合适的 dateStyle 进行日期格式化。例如:…
Read More