How do headless-ui expose context variable for child components?

Hey I'm trying to recreate a similar pattern to what is used in headless-ui Menu components. https://headlessui.com/react/menu
import { Menu } from '@headlessui/react'

function MyDropdown() {
return (
<Menu>
{({ open }) => (
<>
<Menu.Button>More</Menu.Button>
{open && (
<div>
<Menu.Items static>
<Menu.Item>{/* ... */}</Menu.Item>
{/* ... */}
</Menu.Items>
</div>
)}
</>
)}
</Menu>
)
}
import { Menu } from '@headlessui/react'

function MyDropdown() {
return (
<Menu>
{({ open }) => (
<>
<Menu.Button>More</Menu.Button>
{open && (
<div>
<Menu.Items static>
<Menu.Item>{/* ... */}</Menu.Item>
{/* ... */}
</Menu.Items>
</div>
)}
</>
)}
</Menu>
)
}
As you can see open is passable as a variable to be consumed by the children. How can I recreate such a pattern?
Headless UI
Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
8 Replies
Jim
JimOP2y ago
This example probably shows better what I'm trying to achieve (but is wrong):
const MenuContext = createContext({});

function Menu({ children }:{children: React.ReactNode}) {
const [open, setOpen] = useState(false);

return (
<MenuContext.Provider value={{ open, setOpen }}>
{children}
</MenuContext.Provider>
);
}

function Test({ children }:{children: React.ReactNode}) {

return (
<Menu>
{({ open }) => (
<>
<button>Trigger</button>
{open && (
<ul>
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
)}
</>
)}
</Menu>
);
}
const MenuContext = createContext({});

function Menu({ children }:{children: React.ReactNode}) {
const [open, setOpen] = useState(false);

return (
<MenuContext.Provider value={{ open, setOpen }}>
{children}
</MenuContext.Provider>
);
}

function Test({ children }:{children: React.ReactNode}) {

return (
<Menu>
{({ open }) => (
<>
<button>Trigger</button>
{open && (
<ul>
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
)}
</>
)}
</Menu>
);
}
I've not seen this way of passing probs before, seems like headless calls it 'slots' but I can only find Vue style examples Ohh... you can just pass children the value like:
function Menu({ children }:{children: React.ReactNode}) {
const [open, setOpen] = useState(false);

return (
<MenuContext.Provider value={{ open, setOpen }}>
{children({open})}
</MenuContext.Provider>
);
}
function Menu({ children }:{children: React.ReactNode}) {
const [open, setOpen] = useState(false);

return (
<MenuContext.Provider value={{ open, setOpen }}>
{children({open})}
</MenuContext.Provider>
);
}
Does this have a name? :D When I use in typescript, it's saying ReactNodes aren't callable thinkies I just replaced the type like so:
interface MenuProps {
children: (props: { open: boolean }) => React.ReactNode;
}
interface MenuProps {
children: (props: { open: boolean }) => React.ReactNode;
}
Not sure if that is a hack or not lol... ok.. seems like im running into a boundary error. Functions cannot be passed directly to Client Components because they're not serializable. :(
praskoo
praskoo2y ago
I was working on sort-of wrapping the Headless-UI components so that I could create reusable UI components, the goal was to basically have some base classes, but still keep the same pattern, where children could consume props from parents I didn't exactly nail it, I still ended up with some bugs, but I could share some of that code if it helps For example, in the H/UI Listbox component, the children are typed as:
const children: React.ReactNode | ((bag: ListboxRenderPropArg<TType>) => React.ReactElement<any, string | React.JSXElementConstructor<any>>)
const children: React.ReactNode | ((bag: ListboxRenderPropArg<TType>) => React.ReactElement<any, string | React.JSXElementConstructor<any>>)
To me it looks like you were on the right track at least
Jim
JimOP2y ago
Oh nice thank you! I'll have a look at those types, but really my issue now is with server/client rendering with next app.
praskoo
praskoo2y ago
Ah, I'm not sure about server/client rendering I believe my stuff is entirely client rendered. I'll create a gist with the Select component (wrapper around Listbox), maybe it helps.
Jim
JimOP2y ago
Yeah I'm trying to make a suspense preview component 'generic': page.tsx
export default async function Environment({
params,
}: {
params: { slug: string };
}) {
const token = getPreviewToken();
const data = await getEnvironmentPageBySlug({ slug: params.slug });

if (!data && !token) {
notFound();
}

return (
<>
{token ? (
<PreviewTest
token={token}
params={params}
schema={data}
query={EnvironmentPageQuery}
>
{({ data }) => (
<EnvironmentPage page={data[0]} />
)}
</PreviewTest>
) : (
<EnvironmentPage page={data[0]}/>
)}
</>
);
}
export default async function Environment({
params,
}: {
params: { slug: string };
}) {
const token = getPreviewToken();
const data = await getEnvironmentPageBySlug({ slug: params.slug });

if (!data && !token) {
notFound();
}

return (
<>
{token ? (
<PreviewTest
token={token}
params={params}
schema={data}
query={EnvironmentPageQuery}
>
{({ data }) => (
<EnvironmentPage page={data[0]} />
)}
</PreviewTest>
) : (
<EnvironmentPage page={data[0]}/>
)}
</>
);
}
PreviewTest.tsx
"use client";

export function PreviewTest<T extends object>({
token,
params,
query,
schema,
children,
}: {
token: null | string;
query: string;
params: Params;
schema: T;
children: (props: { data: T }) => React.ReactNode;
}) {
const data: T = usePreview(token, query, params);
console.log("page:", data);

if (
(typeof data === "object" && !data) ||
("length" in data && data.length === 0)
) {
return <div className="text-center">Error</div>;
}

return (
<PreviewContext.Provider value={{ data }}>
<PreviewSuspense fallback={<div className="">Loading</div>}>
{children({ data })}
</PreviewSuspense>
</PreviewContext.Provider>
);
}
"use client";

export function PreviewTest<T extends object>({
token,
params,
query,
schema,
children,
}: {
token: null | string;
query: string;
params: Params;
schema: T;
children: (props: { data: T }) => React.ReactNode;
}) {
const data: T = usePreview(token, query, params);
console.log("page:", data);

if (
(typeof data === "object" && !data) ||
("length" in data && data.length === 0)
) {
return <div className="text-center">Error</div>;
}

return (
<PreviewContext.Provider value={{ data }}>
<PreviewSuspense fallback={<div className="">Loading</div>}>
{children({ data })}
</PreviewSuspense>
</PreviewContext.Provider>
);
}
Jim
JimOP2y ago
Thanks I'll check it out!!
thejessewinton
@.3819 I'm running into a similar error here, did you figure anything out?
Want results from more Discord servers?
Add your server