agentsclimarketplace

React headless components

Skill lenguyenhoangkhang2/agent-skills/skills/react-headless-components

Install
npx -y skills add lenguyenhoangkhang2/agent-skills --skill react-headless-components

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.2 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it

React Headless Components (Hook + Component Pattern)

What this is

This is the modern, Hooks-based descendant of the classic Container/Presentational pattern popularized by Dan Abramov. The "Container" role — holding state and logic — used to require a wrapper component (or a HOC). Since Hooks, that role moved into a plain custom hook, and the old Presentational component became a truly stateless render function. The result is usually called the Headless Component Pattern (same idea behind Radix UI, Headless UI, Downshift, TanStack Table/Query): the hook knows nothing about UI and just returns { data, actions }; the component knows nothing about fetching or business logic and just renders what it's handed.

The detail that matters most in practice: wiring happens at the consumer, not inside the feature itself. A page or route imports both the hook and the component and connects them explicitly. Compare this to the classic pattern, where the Container component imported the Presentational component and wired it internally — convenient, but it welds the logic to one specific UI and makes the logic harder to reuse or test on its own.

Classic Container/PresentationalHook + Component (this pattern)
Logic lives inContainer componentCustom hook
Wiring happens inInside the ContainerAt the consumer (page/route)
Reusing logic with different UIHard — must rewrite the containerEasy — the hook doesn't care what renders it

The three pieces

1. The hook (use<Feature>) — owns everything that isn't rendering:

  • Data fetching, async actions, derived state, business rules
  • Returns a small number of semantically grouped values, not a flat list

2. The component (<Feature>) — owns everything that is rendering:

  • Only accepts data and callback props — never fetches, never subscribes, never reaches for global/business state on its own
  • Handles conditional rendering (loading/error/empty/success states) based on the props it's given
  • Splits into sub-components via composition (compound-component style, e.g. Feature.Avatar, Feature.Details) when the UI has distinct visual regions

3. The consumer (a page or route) — owns the wiring:

  • Imports the hook and the component separately
  • Calls the hook, destructures what it needs, passes it into the component as props
  • This is the only place the two are connected — that's what keeps the hook and component independently reusable and independently testable

Worked example

// features/user-profile/useUserProfile.ts
export function useUserProfile(userId: string) {
  const { data, isLoading, error } = useProfileQuery(userId); // any fetching lib
  const [isEditing, setIsEditing] = useState(false);

  async function updateProfile(input: ProfileInput) {
    await api.updateProfile(userId, input);
    setIsEditing(false);
  }

  async function deleteAccount() {
    await api.deleteAccount(userId);
  }

  // Grouped by concern, not returned flat — see "Group hook results" below.
  return {
    profile: { data, isLoading, error },
    actions: { updateProfile, deleteAccount },
    ui: { isEditing, setIsEditing },
  };
}
// features/user-profile/UserProfile.tsx
type UserProfileProps = {
  data: Profile | undefined;
  isLoading: boolean;
  error: Error | null;
  isEditing: boolean;
  onEditToggle: () => void;
  onSave: (input: ProfileInput) => void;
  onDelete: () => void;
};

export function UserProfile(props: UserProfileProps) {
  if (props.isLoading) return <UserProfile.Skeleton />;
  if (props.error) return <UserProfile.Error error={props.error} />;
  if (!props.data) return null;

  return props.isEditing ? (
    <UserProfile.EditForm profile={props.data} onSave={props.onSave} onCancel={props.onEditToggle} />
  ) : (
    <UserProfile.Details profile={props.data} onEdit={props.onEditToggle} onDelete={props.onDelete} />
  );
}

UserProfile.Skeleton = function Skeleton() { /* ... */ };
UserProfile.Error = function ErrorState({ error }: { error: Error }) { /* ... */ };
UserProfile.Details = function Details({ profile, onEdit, onDelete }: /* ... */ any) { /* ... */ };
UserProfile.EditForm = function EditForm({ profile, onSave, onCancel }: /* ... */ any) { /* ... */ };
// app/routes/profile/[userId]/page.tsx  — the consumer does the wiring
export default function ProfilePage({ params }: { params: { userId: string } }) {
  const { profile, actions, ui } = useUserProfile(params.userId);

  return (
    <UserProfile
      data={profile.data}
      isLoading={profile.isLoading}
      error={profile.error}
      isEditing={ui.isEditing}
      onEditToggle={() => ui.setIsEditing(!ui.isEditing)}
      onSave={actions.updateProfile}
      onDelete={actions.deleteAccount}
    />
  );
}

Rules that keep the pattern intact

Group the hook's return value by concern. A hook that returns ten-plus flat values (data, isLoading, error, isEditing, setIsEditing, updateProfile, ...) makes the consumer's wiring unreadable and easy to get wrong. Group into a couple of objects with clear names instead — { profile, actions, ui } reads at a glance; ten positional-feeling destructured values don't.

The hook must never import the component, and the component must never import or call the hook. The moment one references the other, you've quietly rebuilt a Container that auto-wires itself — you lose the ability to reuse the hook with a different UI, render the component from a Storybook story with fake props, or unit test the logic without mounting anything.

The component never fetches, subscribes, or reaches into global state on its own. If you find yourself reaching for useQuery or a store hook inside the presentational component "just this once," that's the pattern eroding — move it into the hook, even if it means threading one more prop through.

Only reach for this when the logic actually earns it. A component that's a couple of useState calls and no fetching doesn't need a separate hook file — that's indirection with no payoff. This pattern pays off once there's real business logic, async work, or a genuine need to reuse the same logic behind more than one UI (web + native, or a redesign). A plain <Button> or <Badge> should stay a plain component.

Folder convention

Colocate the pieces by feature so the pairing is obvious at a glance:

features/user-profile/
  useUserProfile.ts        # the hook — logic, state, data
  UserProfile.tsx          # the component — render + compound sub-components
  UserProfile.stories.tsx  # optional: drives the component with fake props, no hook needed

The consumer (a route/page file elsewhere in the app) is what imports both and wires them together — it does not live inside features/user-profile/.

Also known as

You may see this called "Container/Presentational" (hook replaces the old container component), "Smart Hook, Dumb Component," or "Headless Component Pattern" — same underlying idea, just different eras of React vocabulary. If someone says "container component" today, check whether they mean this hook-based version before assuming they want a literal wrapper component.

See references/further-reading.md for background reading on where this pattern came from and how libraries like Downshift apply it.

What ships with it: 1 file

1.6 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 326,149. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.