Skip to content

wibus-dev/react-aside-playbook

Repository files navigation

Left Aside Developer Guide

Design Principles

  • Single source of truth: tab and path live in the URL so refresh/back/forward/share work; selection and panel stay in memory to avoid leaking transient UI state into the URL.
  • Tight API surface: useLeftAside exposes state, actions, and helpers in one place—no need to juggle multiple contexts.
  • Composable with data layers: helpers provide stable query keys for React Query or other caches; actions are designed to be directly wired to event handlers.

State Model

  • tab: string: active tab.
  • path: PathSegment[]: hierarchical path, persisted per tab as path.<tab> query param (default []).
  • selection: { entity: Entity | null; panelOpen: boolean }: in-memory selection and detail panel state.
  • Provider config (LeftAsideProviderProps):
    • tabs: readonly string[] (required), defaultTab
    • tabKey, pathKey (defaults: tab / path, stored as path.<tab>)
    • initialPath
    • pathCodec (custom parse/serialize)
    • autoOpenPanel (default true), panelEnabled (default true)
    • resetSelectionOnTabChange (default true), resetPathOnTabChange (default false)

Hook API (useLeftAside)

const { tab, path, selection, actions, helpers } = useLeftAside<
  PathSegment,
  Entity
>();

selection.entity; // current selection or null
selection.panelOpen; // panel open flag (auto-false when panel disabled)

actions.setTab(next, { replace, resetPath, resetSelection });
actions.setPath(segments, { replace });
actions.pushPath(segment, { replace });
actions.popPath({ replace });
actions.popTo(index, { replace });
actions.clearPath({ replace });
actions.setSelection(entityOrNull, { openPanel });
actions.clearSelection();
actions.openPanel();
actions.closePanel();
actions.togglePanel();

helpers.segmentAt(index); // PathSegment | undefined
helpers.queryKey(...suffix); // ['left-aside', tab, ...path, ...suffix]

Common Usage

Basic wiring

export function Layout({
  children,
  aside,
}: {
  children: React.ReactNode;
  aside: React.ReactNode;
}) {
  return (
    <LeftAsideProvider
      tabs={["sources", "assets", "queries"]}
      defaultTab="sources"
    >
      <Layout aside={aside}>{children}</Layout>
    </LeftAsideProvider>
  );
}

const Shell = ({
  children,
  aside,
}: {
  children: React.ReactNode;
  aside: React.ReactNode;
}) => {
  const { tab, selection, actions } = useLeftAside<string, Entity>();
  const { panelOpen, entity } = selection;

  return (
    <>
      <Tabs value={tab} onValueChange={actions.setTab} />
      <LeftAsideLayout
        asideContent={aside}
        detailPanel={entity ? <Detail entity={entity} /> : null}
        panelOpen={panelOpen}
      />
      {children}
    </>
  );
};

React Query with path-driven keys

const { path, helpers, actions } = useLeftAside<string, Table>();
const sourceId = path[0];
const tableName = path[1];

const tablesQuery = useQuery({
  queryKey: helpers.queryKey("tables", sourceId),
  queryFn: () => fetchTables(sourceId),
  enabled: Boolean(sourceId),
});

const schemaQuery = useQuery({
  queryKey: helpers.queryKey("schema", sourceId, tableName),
  queryFn: () => fetchSchema(sourceId, tableName),
  enabled: Boolean(sourceId && tableName),
});

const handleSelectSource = (id: string) =>
  actions.setPath([id], { replace: true });
const handleSelectTable = (table: Table) => {
  actions.setPath([table.sourceId, table.name], { replace: true });
  actions.setSelection(table);
};

Detail panel patterns

  • actions.setSelection(entity, { openPanel }): set selection and optionally open panel (defaults to autoOpenPanel).
  • actions.clearSelection(): clear selection and close panel.
  • With panelEnabled=false, selection.panelOpen is always false but selection.entity remains available for layout logic.

Routing & Persistence

  • tab is stored in a query param (default tab).
  • path is stored as path.<tab>—each tab keeps its own history. By default, switching tabs preserves each tab’s path (resetPathOnTabChange toggles this).
  • selection and panelOpen are not persisted; refresh retains tab/path but not the selected entity.

Caveats

  • Follow project Tailwind rules (no dynamic template literals); continue using cn for class composition.
  • Avoid caching actions/helpers in refs; always read from useLeftAside to prevent stale references.

Trade-offs & Extension Points

  • Selection not persisted: keeps transient UI out of sharable URLs. If you need it persisted, write selection.entity.id into a query param at the call site and hydrate via initialPath/setSelection.
  • Query key convention: helpers.queryKey prefixes with ['left-aside', tab, ...path] for consistent caching. If you want stricter isolation, append your own suffix.
  • Panel behavior: autoOpenPanel is click-to-open by default. For explicit “expand” UX, disable autoOpenPanel and call actions.openPanel from the UI.

Quick Best Practices

  • Tabs/paths: use setTab/setPath; don’t hand-roll URL query updates.
  • Selection/panel: use setSelection/clearSelection; don’t manually sync panel state.
  • React Query: drive queryKey from helpers.queryKey or path/selection; gate fetches with enabled.
  • Anti-stale: avoid storing actions/helpers in refs; read fresh per render.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published