denoland / fresh

The next-gen web framework.
https://fresh.deno.dev
MIT License
12.17k stars 621 forks source link

parent.props.children.push is not a function. Island composition. Fresh 2.0 #2624

Open predaytor opened 3 weeks ago

predaytor commented 3 weeks ago

Is it possible to compose multiple exports of a certain island component in Fresh 2.0? It worked at Fresh 1.0. Not sure if this is even a good pattern as we can't rely on render props callbacks etc, when using in a server context.

So the expected behavior for islands to be imported using a single component import or the default export (similar to Next.js client components) and the 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>
    );
}
Знімок екрана 2024-08-20 о 22 24 49
predaytor commented 2 weeks ago

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>
  );
}