Design Challenge 009: Refactoring a Status-Driven UI in React
Untangling status-based rendering logic in a real-world React component
Welcome back to the Design Challenges series!
If you're new here, this series explores practical UI design and architecture problems that come up in everyday frontend development. Each issue presents a real-world React challenge — and the following week, I share a solution and walk through the thought process behind it.
In frontend development, it's common to build UI that changes depending on the current state or status of a process — loading, error, success, or more nuanced stages of a user journey.
But as the number of statuses grows, the code that renders these different views can become messy and hard to maintain. Multiple if
conditions, repeated JSX, and embedded logic inside your component can all add up to something brittle and hard to reason about.
Let me show you a real-world example.
📊 The Feature: Direct To Boot
We're building a feature called Direct To Boot, which allows customers to notify store staff when they've arrived to pick up an order. Depending on the order status, the UI changes:
Initialised: The order is still being prepared. The button is disabled.
Ready: The order is ready. The user can now click the button to say "I'm here."
Notified: A confirmation message is shown after the user clicks the button.
Error: Something went wrong. The user is instructed to call a number.
Each status has different content, and in some cases, different interactions.
🤔 The Current Implementation
Before we dive into the code, here's a quick note about the data-fetching logic we're using.
This component uses React Query to manage both fetching and updating the order status. If you're not familiar with it, React Query provides a declarative way to manage server state in React — it helps with caching, revalidation, error handling, and much more.
useQuery
is used to fetch the current order status from the server. It handles retries, error states, and background refetches.useMutation
is used to trigger an update — in this case, notifying the server when the user clicks "I'm here."
With that in mind, let’s look at the current implementation:
Here's the actual component we wrote to implement this logic:
import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { fetchOrderStatus, updateOrderStatus } from "../api";
export type Status = "initialised" | "ready" | "notified" | "error";
export function DirectToBoot({ orderId }: { orderId: string }) {
const [status, setStatus] = useState<Status>("initialised");
useQuery(["order"], async () => await fetchOrderStatus(orderId), {
initialData: { status: "initialised" },
retry: 5,
refetchOnWindowFocus: false,
onSuccess: () => setStatus("ready"),
onError: () => setStatus("error"),
});
const mutation = useMutation(updateOrderStatus, {
onSuccess: () => setStatus("notified"),
onError: () => setStatus("error"),
});
const getMessage = (status: Status) => {
switch (status) {
case "ready":
return "Please click the button when you have arrived. One of our friendly staff will bring your order to you.";
case "error":
return "Seems something went wrong, you can call the following number to notify us instead.";
case "initialised":
return "We're preparing your order...";
case "notified":
return "Thanks for letting us know, your order will be with you shortly.";
default:
return "";
}
};
return (
<div className="container">
<h3>Direct To Boot</h3>
<p>{getMessage(status)}</p>
<div className="buttonContainer">
{status === "initialised" && (
<button className="primaryButton" disabled>
I'm here
</button>
)}
{status === "ready" && (
<button
className="primaryButton"
onClick={() => mutation.mutate(orderId)}
>
I'm here
</button>
)}
{status === "error" && (
<a href="tel:042333" className="primaryButton">
04-23-33
</a>
)}
</div>
</div>
);
}
While this works, it's starting to get cluttered — status
is checked in multiple places, and it's not obvious which logic goes with which UI.
Let’s break down what the key pieces of this component are doing:
useQuery
fetches the current status of the order when the component mounts. If it succeeds, we assume the order is ready and update the UI accordingly. If it fails, we show an error state.useMutation
is used to send a "customer has arrived" signal to the backend. It updates the status tonotified
on success, orerror
if something fails.getMessage()
maps each status to a user-facing message.The button rendering logic uses conditional rendering based on the current status, with slightly different behaviors and elements.
Overall, the logic works — but it's a bit tangled. Let’s see how we might untangle it.
🌍 Your Challenge
If you were tasked with refactoring this component, how would you structure it to make the code easier to understand, extend, and maintain?
Would you break it into smaller components? Introduce a render map? Use pattern matching libraries?
Reply with your thoughts — or just spend a few minutes thinking about it.
In the next issue, I’ll show you two different refactoring approaches I considered (both of which are featured in the newly updated edition of my book, Maintainable React - use the coupon in this link to get a 50% discount).
Stay tuned for the solution!