Skip to content
ZA

Blog4 min read

How I Stopped a Booking Flow From Becoming a State Management Nightmare

I share how I structured shared state across a flight and hotel booking journey, keeping search, selections, traveller details, pricing, and temporary UI state from turning into one confusing frontend problem.

Frontend architecture illustration showing search, flight, hotel, travellers, extras, and payment stages connected to one shared booking state in a travel booking flow.
A single booking state keeps search, flight, hotel, traveller, extra, and payment data connected throughout the booking journey.

A flight or hotel booking flow looks straightforward from the outside.

Search. Select. Add travellers. Pay. Done.

But behind those few steps, the frontend is quietly carrying a lot of important information: trip type, dates, passengers, selected flights, hotel rooms, extras, traveller details, prices, currency, and payment-related data.

If that information is not structured properly, the booking flow can become a very expensive game of “where did this value come from?”

I have seen how quickly this can happen in a travel product. One screen updates the selected flight, another screen still shows an old fare, and a third screen has its own copy of the passenger count. Everything looks fine until the user goes back one step, changes something, and suddenly the summary starts telling a different story.

That is when I realised I should not treat every booking page as a separate screen with its own little world.

I needed to treat the whole journey as one connected product flow.

The problem: too many screens, too much shared data

A typical booking journey can look like this:

text
Search → Results → Flight selection → Hotel selection → Travellers → Extras → Payment → Confirmation

Every step needs information from the previous step. Some steps can also change information that affects everything afterwards.

For example:

  • Changing a flight can change the total price.
  • Changing passenger count can affect flight availability and room occupancy.
  • Adding a hotel room changes the booking summary.
  • Adding extras changes the final amount.
  • Editing traveller details must be reflected before payment.

If every component keeps its own version of the same data, bugs are almost guaranteed. Not immediately, of course. They prefer waiting until a user is about to pay.

The change I made: one booking state, clear ownership

Instead of passing data through multiple components or storing similar values in different places, I separated the booking journey into clear state areas.

bookingState = {
  search: {
    tripType: 'return',
    routes: [],
    dates: {},
    passengers: {},
    cabinClass: 'economy',
  },

  selection: {
    flights: {},
    hotel: {},
    rooms: [],
    extras: [],
  },

  travellers: [],

  pricing: {
    currency: 'AED',
    total: 0,
    breakdown: [],
  },
};

This gave the flow one reliable source of truth.

The search section owned only search-related data. The selection section owned what the customer chose. Traveller details had their own place. Pricing was treated as its own important area instead of being recalculated randomly by whichever component felt motivated that day.

Why this made the frontend easier to manage

Each booking screen could now read the data it needed and update it through controlled actions.

selectFlight(flight);
selectHotel(hotel);
updateTravellers(travellers);
addExtra(extra);
updatePricing(priceBreakdown);

This helped avoid a few common problems:

  • The flight results page was not responsible for hotel state.
  • The payment page did not need to guess which flight was selected.
  • The booking summary always read from the same booking data.
  • Going back to a previous step did not wipe the entire journey.
  • A change in one step could safely update dependent information.

The result was not just cleaner code. It made the booking experience more predictable for users too.

Keeping UI state separate from booking data

One thing that also helped was separating real booking data from temporary UI state.

For example, these are not booking data:

uiState = {
  isFareDrawerOpen: false,
  isLoadingResults: false,
  activeTab: 'flights',
  validationErrors: {},
};

A drawer being open should not live beside a selected flight. A loading spinner should not be part of the traveller object. It sounds obvious when written down, but in a large booking flow, these small boundaries save a lot of debugging time.

The lesson I took from it

The booking flow was not a set of pages. It was one connected journey with shared data moving through multiple steps.

Once I treated it that way, the architecture became easier to extend. Adding hotel selection, extras, multi-city flights, or new validation rules did not mean creating another disconnected state island.

It meant adding to a structure that already understood how booking data should move.

And honestly, in travel products, that is important. Users already have enough things to think about flight times, baggage, room types, passport details, and whether their connection is only 45 minutes.

They should not also have to worry about our frontend forgetting what they selected two screens ago.