Avoid using boolean isLoading react state
Table of contents
Open Table of contents
Introduction
Why Use Complex Loading States? By using a complex loadingState, you can explicitly track the various phases of an asynchronous operation (such as a form submission or data fetching), and handle combinations of states in a way that matches your business logic.
Common Complex States:
Idle: The component is in its initial state, waiting for user action or data fetching to start. Pending/Loading: The operation is currently in progress (e.g., fetching data or submitting a form). Success: The operation completed successfully, and the result is ready to be displayed. Error: An error occurred, and the UI should display an error message. By managing these states separately, you ensure that all possible outcomes of an asynchronous operation are handled appropriately.
Defining Complex States in React
Let’s define a loadingState that can have values like “idle”, “pending”, “success”, and “error”. Based on this state, we’ll derive more granular flags like isLoading, isSuccessful, or hasError, which will determine what gets rendered.
Example:
import React, { useState, useEffect } from "react";
function ComplexStateDataFetcher() {
const [data, setData] = useState(null);
const [loadingState, setLoadingState] = useState("idle"); // idle | pending | success | error
useEffect(() => {
const fetchData = async () => {
setLoadingState("pending"); // Operation has started
try {
const response = await fetch("https://api.example.com/data");
const result = await response.json();
setData(result);
setLoadingState("success"); // Operation completed successfully
} catch (error) {
setLoadingState("error"); // An error occurred
}
};
fetchData();
}, []);
// Derived states based on loadingState
const isLoading = loadingState === "pending" || loadingState === "idle";
const isSuccessful = loadingState === "success";
const hasError = loadingState === "error";
return (
<div>
{isLoading && <Spinner />} {/_ Show spinner when loading _/}
{hasError && <ErrorMessage />} {
/_ Show error message if something went wrong _/
}
{isSuccessful && <DataDisplay data={data} />}{" "}
{/_ Show data when successful _/}
</div>
);
}
Key Breakdown:
loadingState: Tracks the current state of the operation (idle, pending, success, or error). Derived states: isLoading: True when the operation is either starting (idle) or in progress (pending). isSuccessful: True when the operation completed successfully (success). hasError: True when an error occurred (error). Conditional rendering: The UI dynamically changes based on the derived states (isLoading, isSuccessful, hasError). Handling Multiple Loading Scenarios You can also use this approach when dealing with multiple asynchronous operations within the same component. For instance, handling both data fetching and form submission states in parallel.
function FormAndDataFetcher() {
const [formState, setFormState] = useState("idle"); // idle | pending | success | error
const [dataState, setDataState] = useState("idle"); // idle | pending | success | error
const [data, setData] = useState(null);
const fetchData = async () => {
setDataState("pending");
try {
const response = await fetch("https://api.example.com/data");
const result = await response.json();
setData(result);
setDataState("success");
} catch (error) {
setDataState("error");
}
};
const submitForm = async formData => {
setFormState("pending");
try {
await fetch("https://api.example.com/submit", {
method: "POST",
body: JSON.stringify(formData),
headers: { "Content-Type": "application/json" },
});
setFormState("success");
} catch (error) {
setFormState("error");
}
};
// Derived states
const isFormLoading = formState === "pending" || formState === "idle";
const isDataLoading = dataState === "pending" || dataState === "idle";
return (
<div>
{/_ Data Fetching Section _/}
{isDataLoading && <Spinner />}
{dataState === "error" && <ErrorMessage />}
{dataState === "success" && <DataDisplay data={data} />}
{/* Form Section */}
<form
onSubmit={e => {
e.preventDefault();
submitForm({ name: "John" });
}}
>
<button type="submit" disabled={isFormLoading}>
{isFormLoading ? "Submitting..." : "Submit"}
</button>
</form>
{formState === "error" && <p>Form submission failed.</p>}
{formState === "success" && <p>Form submitted successfully!</p>}
</div>
);
}
In this example, you have two independent asynchronous operations, each with its own complex state:
dataState: Tracks the data fetching process. formState: Tracks the form submission process. Each state can be in one of the four stages: idle, pending, success, or error. The derived states isFormLoading and isDataLoading handle the combinations of these states, ensuring that the UI reflects the current status accurately.
Why This Approach Works for Complex Business Logic When you have complex business logic, such as handling multiple asynchronous requests or ensuring data and form submissions interact smoothly, managing these different states with a simple boolean like isLoading can become unmanageable. Using a complex loadingState that tracks combinations of states allows you to address business requirements more effectively.
For example, there might be a case where your business logic requires showing a specific UI when a request is pending, but the form is idle. Or, you might need to display different messages based on success or error states. A single boolean flag would not be enough to handle these cases cleanly.
Handling Multiple Outcomes:
Pending or Idle: Used to display a loading spinner or disable form elements until the operation is complete. Success: The desired outcome has occurred, and the UI can now display the data or confirmation. Error: An error occurred, and the UI should provide feedback to the user. Idle: The component is waiting for an action to trigger the operation.
// Tracking business logic through complex state handling
const isLoading = loadingState === "pending" || loadingState === "idle";
const isSuccess = loadingState === "success";
const isError = loadingState === "error";
// In the render method, these derived states would dictate which UI to show:
if (isLoading) {
return <Spinner />;
}
if (isError) {
return <ErrorMessage />;
}
if (isSuccess) {
return <DataDisplay />;
}
Conclusion
Using a complex loadingState rather than simple boolean flags like isLoading gives you more granular control over your component’s state. It allows for handling more nuanced business logic, such as displaying different UIs based on whether a request is idle, pending, successful, or has failed. This approach ensures that all potential states are covered, reduces the likelihood of undetected states, and improves the robustness of your asynchronous flows in React.