Skip to content

info-mdshakeeb/keep-render

Repository files navigation

keep-render

A React package that simplifies conditional rendering. It provides declarative components for handling loading, error, and data states — making your JSX more readable and maintainable with less boilerplate.

Installation

npm install keep-render

Quick Start

import { Render } from "keep-render";

function App() {
  const isLoading = false;
  const isError = false;
  const hasData = true;

  return (
    <Render isLoading={isLoading} isError={isError}>
      <Render.Loading>
        <div>Loading...</div>
      </Render.Loading>

      <Render.Error>
        <div>Something went wrong</div>
      </Render.Error>

      <Render.When isTrue={hasData}>
        <div>Data is available</div>
      </Render.When>

      <Render.Else>
        <div>No data available</div>
      </Render.Else>
    </Render>
  );
}

Examples

Standalone condition

Render.When can be used outside of Render for simple inline conditions.

<Render.When isTrue={user.isAdmin}>
  <AdminBadge />
</Render.When>

Typed API response with Render.Data

Render.Data extracts response.data and passes it to a callback. The second argument tells you whether data is still loading.

type User = { id: string; name: string; email: string };

function UserHeader({
  response,
  isLoading,
}: {
  response?: { data?: User };
  isLoading: boolean;
}) {
  return (
    <Render isLoading={isLoading}>
      <Render.Data response={response}>
        {(user, loading) =>
          loading ? <UserSkeleton /> : <UserCard user={user} />
        }
      </Render.Data>

      <Render.Else>No user found</Render.Else>

      <Render.Loading>
        <UserSkeleton />
      </Render.Loading>
    </Render>
  );
}

If you don't need the response value, pass normal JSX children:

<Render.Data response={response}>
  <UserSummary />
</Render.Data>

Error handling

Render.Error renders when the parent Render receives isError={true}. Error has priority over Render.Data, Render.When, and Render.Else. Loading still takes top priority.

<Render isLoading={isLoading} isError={isError}>
  <Render.Error>
    <div role="alert">Could not load user.</div>
  </Render.Error>

  <Render.Data response={response}>
    {(user) => <UserCard user={user} />}
  </Render.Data>

  <Render.Else>No user found</Render.Else>
</Render>

Nested conditions

<Render>
  <Render.When isTrue={condition1}>
    <div>Condition 1 met</div>
    <Render>
      <Render.When isTrue={condition2}>
        <div>Condition 2 also met</div>
      </Render.When>
      <Render.Else>Condition 2 not met</Render.Else>
    </Render>
  </Render.When>
  <Render.Else>
    <div>No conditions met</div>
  </Render.Else>
</Render>

Nested permission checks

<Render>
  <Render.Data response={{ data: session?.user }}>
    {(user) => (
      <Render>
        <Render.When isTrue={user.role === "admin"}>
          <AdminPanel user={user} />
        </Render.When>
        <Render.Else>
          <UserPanel user={user} />
        </Render.Else>
      </Render>
    )}
  </Render.Data>

  <Render.Else>Please sign in.</Render.Else>
</Render>

Complex Examples

API table with error, empty, data, and pagination

type Product = { id: string; name: string; stock: number };
type ProductResponse = { results: Product[]; total: number };

function ProductTable({
  data,
  isLoading,
  isError,
}: {
  data?: ProductResponse;
  isLoading: boolean;
  isError: boolean;
}) {
  return (
    <Render isLoading={isLoading}>
      <Render.When isTrue={isError}>
        <div role="alert">Could not load products.</div>
      </Render.When>

      <Render.When isTrue={Boolean(data && data.results.length === 0)}>
        <div>No products found.</div>
      </Render.When>

      <Render.Data response={{ data }}>
        {(products, loading) => (
          <section>
            {loading ? (
              <div>Loading products...</div>
            ) : (
              <>
                <table>
                  <tbody>
                    {products.results.map((p) => (
                      <tr key={p.id}>
                        <td>{p.name}</td>
                        <td>{p.stock}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>

                <Render.When isTrue={products.total > 10}>
                  <Pagination total={products.total} />
                </Render.When>
              </>
            )}
          </section>
        )}
      </Render.Data>

      <Render.Else>No data</Render.Else>
    </Render>
  );
}

Multi-step wizard form

Handle complex form flows where each step has its own loading, validation, and error states.

type Step = "info" | "address" | "confirm";

function CheckoutWizard({
  step,
  userResponse,
  addressResponse,
  orderResponse,
  isSubmitting,
  submitError,
}: {
  step: Step;
  userResponse?: { data?: { name: string; email: string } };
  addressResponse?: { data?: { city: string; zip: string } };
  orderResponse?: { data?: { orderId: string } };
  isSubmitting: boolean;
  submitError: boolean;
}) {
  return (
    <Render isLoading={isSubmitting} isError={submitError}>
      <Render.Loading>
        <div>Processing your order...</div>
      </Render.Loading>

      <Render.Error>
        <div role="alert">Payment failed. Please try again.</div>
      </Render.Error>

      <Render.When isTrue={step === "info"}>
        <Render isLoading={!userResponse}>
          <Render.Data response={userResponse}>
            {(user, loading) =>
              loading ? <FormSkeleton /> : <UserInfoForm defaultValues={user} />
            }
          </Render.Data>
          <Render.Loading>
            <FormSkeleton />
          </Render.Loading>
        </Render>
      </Render.When>

      <Render.When isTrue={step === "address"}>
        <Render.Data response={addressResponse}>
          {(address) => <AddressForm defaultValues={address} />}
        </Render.Data>
      </Render.When>

      <Render.When isTrue={step === "confirm"}>
        <Render.Data response={orderResponse}>
          {(order) => <h2>Order #{order.orderId} confirmed!</h2>}
        </Render.Data>
      </Render.When>

      <Render.Else>
        <div>Unknown step</div>
      </Render.Else>
    </Render>
  );
}

Dashboard with role-based access control

Combine authentication, role checks, and feature flags in a single declarative tree.

type User = { role: "free" | "pro" | "admin"; name: string };

function AnalyticsDashboard({
  session,
  analyticsResponse,
  isLoading,
  isError,
}: {
  session?: { data?: User };
  analyticsResponse?: { data?: { visits: number; revenue: number } };
  isLoading: boolean;
  isError: boolean;
}) {
  return (
    <Render>
      <Render.Data response={session}>
        {(user) => (
          <Render isLoading={isLoading} isError={isError}>
            <Render.Loading>
              <DashboardSkeleton />
            </Render.Loading>

            <Render.Error>
              <div role="alert">Failed to load analytics.</div>
            </Render.Error>

            <Render.When isTrue={user.role === "admin"}>
              <Render.Data response={analyticsResponse}>
                {(stats) => (
                  <>
                    <StatsCard title="Visits" value={stats.visits} />
                    <StatsCard title="Revenue" value={`$${stats.revenue}`} />
                    <AdminControls />
                  </>
                )}
              </Render.Data>
            </Render.When>

            <Render.When isTrue={user.role === "pro"}>
              <Render.Data response={analyticsResponse}>
                {(stats) => (
                  <>
                    <StatsCard title="Visits" value={stats.visits} />
                    <StatsCard title="Revenue" value={`$${stats.revenue}`} />
                  </>
                )}
              </Render.Data>
            </Render.When>

            <Render.Else>
              <UpgradePrompt currentPlan={user.role} />
            </Render.Else>
          </Render>
        )}
      </Render.Data>

      <Render.Else>
        <LoginPrompt />
      </Render.Else>
    </Render>
  );
}

CRUD table with inline editing

Row-level state management using Render inside a .map() loop.

type Employee = { id: string; name: string; department: string };

function EmployeeTable({
  response,
  isLoading,
  isError,
  editingId,
  deletingId,
  onEdit,
  onDelete,
  onSave,
}: {
  response?: { data?: Employee[] };
  isLoading: boolean;
  isError: boolean;
  editingId: string | null;
  deletingId: string | null;
  onEdit: (id: string) => void;
  onDelete: (id: string) => void;
  onSave: (emp: Employee) => void;
}) {
  return (
    <Render isLoading={isLoading} isError={isError}>
      <Render.Loading>
        <TableSkeleton rows={5} cols={3} />
      </Render.Loading>

      <Render.Error>
        <div role="alert">Could not load employees.</div>
      </Render.Error>

      <Render.Data response={response}>
        {(employees) => (
          <Render>
            <Render.When isTrue={employees.length === 0}>
              <div>No employees found.</div>
            </Render.When>

            <Render.Else>
              <table>
                <tbody>
                  {employees.map((emp) => (
                    <tr key={emp.id}>
                      <Render>
                        <Render.When isTrue={editingId === emp.id}>
                          <td><input defaultValue={emp.name} /></td>
                          <td><input defaultValue={emp.department} /></td>
                          <td><button onClick={() => onSave(emp)}>Save</button></td>
                        </Render.When>

                        <Render.When isTrue={deletingId === emp.id}>
                          <td colSpan={3}>Deleting...</td>
                        </Render.When>

                        <Render.Else>
                          <td>{emp.name}</td>
                          <td>{emp.department}</td>
                          <td>
                            <button onClick={() => onEdit(emp.id)}>Edit</button>
                            <button onClick={() => onDelete(emp.id)}>Delete</button>
                          </td>
                        </Render.Else>
                      </Render>
                    </tr>
                  ))}
                </tbody>
              </table>
            </Render.Else>
          </Render>
        )}
      </Render.Data>

      <Render.Else>No data available</Render.Else>
    </Render>
  );
}

Real-time notification feed

Handle WebSocket connection states, empty feeds, and live updates with nested Render inside error UI.

type Notification = { id: string; message: string; read: boolean };
type WsStatus = "connecting" | "connected" | "disconnected" | "error";

function NotificationFeed({
  status,
  notifications,
}: {
  status: WsStatus;
  notifications: Notification[];
}) {
  const unread = notifications.filter((n) => !n.read);

  return (
    <Render
      isLoading={status === "connecting"}
      isError={status === "error" || status === "disconnected"}
    >
      <Render.Loading>
        <div>Connecting to live feed...</div>
      </Render.Loading>

      <Render.Error>
        <div role="alert">
          <Render>
            <Render.When isTrue={status === "disconnected"}>
              <p>Connection lost. Reconnecting...</p>
            </Render.When>
            <Render.Else>
              <p>Failed to connect. Please refresh.</p>
            </Render.Else>
          </Render>
        </div>
      </Render.Error>

      <Render.When isTrue={notifications.length === 0}>
        <div>No notifications yet.</div>
      </Render.When>

      <Render.When isTrue={notifications.length > 0}>
        <>
          <Render.When isTrue={unread.length > 0}>
            <span className="badge">{unread.length} new</span>
          </Render.When>
          <ul>
            {notifications.map((n) => (
              <li key={n.id} className={n.read ? "read" : "unread"}>
                {n.message}
              </li>
            ))}
          </ul>
        </>
      </Render.When>
    </Render>
  );
}

Before / After Comparison

// ❌ Traditional ternary nesting
{
  isLoading ? (
    <Loading />
  ) : (
    <Col span={24}>
      {(!isLoading && isError) || data?.data?.results?.length === 0 ? (
        <div>Sorry! No Flights Found.</div>
      ) : (
        <>
          {data?.data?.results?.map((item, i) => (
            <FlightCard Item={item} key={i} />
          ))}
          {(data?.count || 0) > 5 && (
            <Pagination total={data?.count || 0} onChange={handlePageChange} />
          )}
        </>
      )}
    </Col>
  );
}

// ✅ Using keep-render
<Render isLoading={isLoading}>
  <Render.When isTrue={isError || data?.data?.results?.length === 0}>
    <div>Sorry! No Flights Found.</div>
  </Render.When>

  <Render.Else>
    {data?.data?.results?.map((item, i) => (
      <FlightCard Item={item} key={i} />
    ))}
    <Render.When isTrue={(data?.count || 0) > 5}>
      <Pagination total={data?.count || 0} onChange={handlePageChange} />
    </Render.When>
  </Render.Else>

  <Render.Loading>
    <Loading />
  </Render.Loading>
</Render>

API Reference

<Render>

The root wrapper. Accepts optional isLoading and isError props.

Prop Type Description
isLoading boolean When true and Render.Loading exists, replaces all content
isError boolean When true and Render.Error exists, shows error content

Priority order: Loading → Error → When/Data matches → Else

<Render.When>

Renders children when isTrue is truthy. Can be used standalone outside Render.

Prop Type Description
isTrue boolean Condition to evaluate

<Render.Data>

Extracts response.data and passes it to a render callback.

Prop Type Description
response { data?: T } API response object
children (data: T, isLoading: boolean) => ReactNode or ReactNode Callback or JSX

<Render.Error>

Renders when parent Render has isError={true}.

<Render.Else>

Fallback content when no Render.When or Render.Data matches.

<Render.Loading>

Full content replacement when parent Render has isLoading={true}.

Context

Render uses React context internally. Render.Else, Render.Error, and Render.Loading must be used within a Render. Render.When works both inside and outside Render.

License

MIT

About

keep-render is a React package that simplifies conditional rendering. It provides components for handling loading states, and for rendering content based on conditions.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors