modern-app-patterns

Compound Components

Expose a component family under a single namespace to share state and behavior.

Pattern

Example

// Select.tsx
import React, { createContext, useContext, useState } from "react";

type Ctx = { value: string | null; set: (v: string) => void };
const C = createContext<Ctx | null>(null);

function Root({ children }: { children: React.ReactNode }) {
  const [value, set] = useState<string | null>(null);
  return <C.Provider value={{ value, set }}>{children}</C.Provider>;
}

function Option({
  value,
  children,
}: {
  value: string;
  children: React.ReactNode;
}) {
  const ctx = useContext(C)!;
  const selected = ctx.value === value;
  return (
    <div onClick={() => ctx.set(value)} aria-selected={selected}>
      {children}
    </div>
  );
}

export const Select = Object.assign(Root, { Option });

Why it works


Live end-to-end example (copy/paste)

Make a small Select using RN primitives and shared context.

// components/Select.tsx
import React, { createContext, useContext, useState } from "react";
import { Pressable, Text, View } from "react-native";

type Ctx = { value: string | null; set: (v: string) => void };
const C = createContext<Ctx | null>(null);

function Root({
  children,
  initialValue = null,
}: {
  children: React.ReactNode;
  initialValue?: string | null;
}) {
  const [value, set] = useState<string | null>(initialValue);
  return <C.Provider value={{ value, set }}>{children}</C.Provider>;
}

function Option({
  value,
  children,
}: {
  value: string;
  children: React.ReactNode;
}) {
  const ctx = useContext(C);
  if (!ctx) throw new Error("Select must be used within Select.Root");
  const selected = ctx.value === value;
  return (
    <Pressable
      onPress={() => ctx.set(value)}
      style={{
        padding: 8,
        backgroundColor: selected ? "#def" : "#eee",
        marginVertical: 4,
      }}
    >
      <Text>{children}</Text>
    </Pressable>
  );
}

function Value() {
  const ctx = useContext(C);
  if (!ctx) throw new Error("Select must be used within Select.Root");
  return <Text>Selected: {ctx.value ?? ""}</Text>;
}

export const Select = Object.assign(Root, { Option, Value });
// screens/SelectDemo.tsx
import React from "react";
import { View } from "react-native";
import { Select } from "../components/Select";

export function SelectDemo() {
  return (
    <View style={{ padding: 16 }}>
      <Select initialValue="b">
        <Select.Value />
        <Select.Option value="a">Alpha</Select.Option>
        <Select.Option value="b">Beta</Select.Option>
        <Select.Option value="g">Gamma</Select.Option>
      </Select>
    </View>
  );
}

Notes

Sandbox copy map

Paste into an Expo app (see sandboxes/react-native-expo):