parent.props.children.push is not a function. Island composition. Fresh 2.0
predaytor opened this issue · 2 comments
Is it possible to compose multiple exports of a certain island component in Fresh 2.0? It worked in Fresh 1.0. It's not a common pattern and would not work for some scenarios, as we can't rely on render callback props when using in a server context, for example.
Seems the expected behavior for islands in 2.0 is to be imported using a single import (aka the default import), similar to Next.js client components, and only pass serializable props with children?
islands/collapsible.tsx
:
import { ComponentChildren, createContext } from "preact";
import { useContext, useId, useState } from "preact/hooks";
interface CollapsibleDataSet {
"data-expanded": string | undefined;
"data-closed": string | undefined;
}
interface CollapsibleContextValue {
dataset: CollapsibleDataSet;
isOpen: boolean;
contentId: string | undefined;
toggle: () => void;
}
const CollapsibleContext = createContext<CollapsibleContextValue | null>(null);
///
export interface CollapsibleRootProps {
defaultOpen?: boolean;
children?: ComponentChildren;
}
export function CollapsibleRoot(props: CollapsibleRootProps) {
const contentId = useId();
const [isOpen, setIsOpen] = useState(false);
function toggle() {
setIsOpen((value) => !value);
}
const dataset: CollapsibleDataSet = {
"data-expanded": isOpen ? "" : undefined,
"data-closed": !isOpen ? "" : undefined,
};
const context: CollapsibleContextValue = {
dataset,
isOpen,
contentId,
toggle,
};
return (
<CollapsibleContext.Provider value={context}>
<div {...dataset} data-x-collapsible="">
{props.children}
</div>
</CollapsibleContext.Provider>
);
}
///
export interface CollapsibleTriggerProps {
children?: ComponentChildren;
}
export function CollapsibleTrigger(props: CollapsibleTriggerProps) {
const context = useContext(CollapsibleContext)!;
return (
<button
onClick={context.toggle}
aria-expanded={context.isOpen}
aria-controls={context.contentId}
data-x-collapsible-trigger=""
>
{props.children}
</button>
);
}
///
export interface CollapsibleContentProps {
children?: ComponentChildren;
}
export function CollapsibleContent(props: CollapsibleContentProps) {
const context = useContext(CollapsibleContext)!;
return (
<div id={context.contentId} data-x-collapsible-content="">
{props.children}
</div>
);
}
routes/index.tsx
:
import {
CollapsibleContent,
CollapsibleRoot,
CollapsibleTrigger,
} from "../islands/collapsible.tsx";
export default function Home() {
// does work without passing any props
// return (
// <div class="px-8 py-8">
// <CollapsibleRoot>
// <CollapsibleTrigger></CollapsibleTrigger>
// <CollapsibleContent></CollapsibleContent>
// </CollapsibleRoot>
// </div>
// );
// throws error
return (
<div class="px-8 py-8">
<CollapsibleRoot>
<CollapsibleTrigger>X</CollapsibleTrigger>
<CollapsibleContent>
<div>Text</div>
</CollapsibleContent>
</CollapsibleRoot>
</div>
);
}
Also, similar issue, the example below works perfectly on Fresh 1.0, console.log
logs out on both server and client (as it is an island component), but on Fresh 2.0 only the server logs out and the client has an empty {}
object .
from: https://deno-blog.com/Using_Preact_Signals_with_Fresh.2022-11-01
state.ts
:
import { type Signal, signal } from '@preact/signals';
export type AppStateType = {
isMainDrawerOpen: Signal<boolean>;
};
export function createAppState(): AppStateType {
const isMainDrawerOpen = signal(false);
return {
isMainDrawerOpen,
};
}
/islands/app-state-provider.tsx
:
import { ComponentChildren, createContext } from 'preact';
import { AppStateType, createAppState } from '../state.ts';
export const AppStateContext = createContext<AppStateType>({} as AppStateType);
export function AppStateProvider(props: { children?: ComponentChildren; }) {
const state = createAppState();
console.log(state);
return (
<AppStateContext.Provider value={state}>
{props.children}
</AppStateContext.Provider>
);
}
/routes/_app.tsx
:
import { type PageProps } from "$fresh/server.ts";
import { AppStateProvider } from "../islands/app-state-provider.tsx";
export default function App({ Component }: PageProps) {
return (
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fresh-project</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<AppStateProvider>
<Component />
</AppStateProvider>
</body>
</html>
);
}
A more straightforward example: I need to expose a dialog trigger through composition, rather than props on a single island component. This was possible in 1.0 Fresh.
/islands/subscribe-form.tsx
:
export function SubscribeFormDialog(props: { children?: ComponentChildren; }) {}
export function SubscribeFormDialogTrigger({ className, ...props }: ComponentProps<typeof Dialog.Trigger>) {
return <Dialog.Trigger className={cx('subscribe-form', 'dialog-trigger', className)} {...props} />;
}
/components/footer.tsx
:
<SubscribeFormDialog triggerProps={{ variant: 'ghost-gray' }}>
<Text size="md" trim="end">subscribe</Text>
</SubscribeFormDialog>
// vs
<SubscribeFormDialog>
<SubscribeFormDialogTrigger variant="ghost-gray">
<Text size="md" trim="end">subscribe</Text>
</SubscribeFormDialog.Trigger>
</SubscribeFormDialog>