Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unable to use preact context in fresh #983

Open
jeankhawand opened this issue Jan 12, 2023 · 11 comments
Open

unable to use preact context in fresh #983

jeankhawand opened this issue Jan 12, 2023 · 11 comments

Comments

@jeankhawand
Copy link

jeankhawand commented Jan 12, 2023

I managed to use preactcreateContext hook as the following:
hooks/ShoppingCartProvider.tsx

export const ShoppingCartContext = createContext({} as ShoppingCart);
[...]
  return (
    <ShoppingCartContext.Provider
      value={{
        cartItems: cartProducts,
        getItemQuantity,
        getTotalCost,
        decreaseItemQuantity,
        removeItemFromCart,
        addItemToCart
      }}
    >
      {props.children}
    </ShoppingCartContext.Provider>
  );

and then tried to import it in an island like this

[...]
import { ShoppingCartContext } from "../hooks/ShoppingCartProvider.tsx";
[...]
const cart = useContext(ShoppingCartContext);

also encapsulate the _app.tsx with the provider

[...]
  return (
    <ShoppingCartProvider>
    <>
        <Head>
          <link rel="stylesheet" href="/app.css" />
        </Head>     
        <Component />
    </>
    </ShoppingCartProvider>
  );

and then run deno start task I am getting the following error

Uncaught TypeError: L2(...).getCart() is not a function

which refers to this particular line L2(ShoppingCartContext).getCart()
Is it something expect? I tried the same scenario with nextjs and it is working as expected with context accessible everywhere.
Thanks in advance 😃

@nesterow
Copy link

nesterow commented Feb 5, 2023

I think this is because context provider wouldn't be hydrated, so calling useContext in an island results in default values on the client. If I get the island's architecture right then it is rather a feature than a bug. Would be nice if it were highlighted in the documentation though.

I came up with a hacky approach using a component that injects shared context and a custom hook. There might be a better solution involving plugins.

The plugin example.

Hope it helps.

@jeankhawand
Copy link
Author

jeankhawand commented Feb 9, 2023

I think this is because context provider wouldn't be hydrated, so calling useContext in an island results in default values on the client. If I get the island's architecture right then it is rather a feature than a bug. Would be nice if it were highlighted in the documentation though.

I came up with a hacky approach using a component that injects shared context and a custom hook. There might be a better solution involving plugins.
Hope it helps.

Hey, that's what I was thinking about, most probably due to island architecture. Implementing Cart context would be tricky in this case.
Will check your approach and keep you updated
Thanks 👍

@thegaryroberts
Copy link

I do have Preact Context working fine with islands. Although I'm using the equivalent of ShoppingCartContext.Consumer instead of the useContext hook.

I think your primary problem is the usage of functions for parameters, which are unsupported in islands:
https://fresh.deno.dev/docs/concepts/islands, Specifically:

Passing props to islands is supported, but only if the props are JSON serializable. This means that you can only pass primitive types, plain objects, and arrays. It is currently not possible to pass complex objects like Date, custom classes, or functions.

@nesterow
Copy link

I've created a plugin to use shared context in fresh, mainly for the purpose of providing globals. It works with useContext, but still, the values must be JSON serializable and you can only use only one context provider.
The test alpha version is here. If it proves to be stable I release it as a separate module.

@ooker777
Copy link

In one Stack Overflow question @marvinhagemeister says that Fresh currently doesn't support passing context values from the server to the client. I wonder why this is the case? Reading about the differences between Fresh and Preact I don't get why Fresh would lack of feature of what Preact already has. Especially on an important hook like useContext(). After all Fresh is a wrapper around Preact.

@marvinhagemeister
Copy link
Collaborator

@ooker777 You can use context on the server in Fresh, but you cannot use context to pass data from the server to the client. This has nothing to do with with Preact, but mainly that we didn't have time to look at how to support that in Fresh yet. Preact is just a rendering library, it has no concept of how to pass data from the server to the client. Preact has no idea of a server and that's exactly where Fresh comes in.

Yes, Fresh is a wrapper around Preact and that Fresh wrapper introduces the notion of Server and a Client that Preact isn't aware of, because it's mostly just a rendering library.

@ooker777
Copy link

ooker777 commented Jan 15, 2024

But isn't that the concept of island or partial hydration involves the interaction between server and client? How can Preact apply those concepts without having any idea about server? Where does the rendering happen? Does React have no idea of a server as well?

@marvinhagemeister
Copy link
Collaborator

marvinhagemeister commented Jan 15, 2024

But isn't that the concept of island or partial hydration involves the interaction between server and client? How can Preact apply those concepts without having any idea about server?

Yep, islands and partial hydration involve the interaction between server and client. Thing is that those concepts are implemented in Fresh, not in Preact. Fresh calls Preact to say "render component X with props Y", but Preact has no idea where those come from, what they represent etc. That those are islands and that the props have been sent from the server to the browser is something only Fresh knows about. Fresh takes care of all of that and triggers Preact's rendering when all is done.

Does React have no idea of a server as well?

React has no idea of servers. It's the meta-frameworks around it like Next.js, Remix and other's that introduce the concept of a server and a client and the glue to make them work together. This distinction is true for other frontend frameworks as well. The frameworks themselves mostly provide only a rendering layer and the meta-frameworks around it provide the rest.

Framework Meta Framework
Preact Fresh
React Next.js, Remix, Gatsby...
Vue Nuxt
Svelte SvelteKit
Solid SolidStart

Routing and all the other typical needs of an app are provided by the right column, not the left. The original frameworks in the left column only deal with rendering and reacting to state updates.

@marvinhagemeister marvinhagemeister removed this from the Fresh 1.5 milestone Jan 15, 2024
@vicary
Copy link

vicary commented Feb 17, 2024

There are use cases that is almost impossible to separate cleanly between server and client.

Imagine a use case of an interactive form with i18n where sections in the form are conditionally rendered via Signals, while text, labels, placeholders have to be translated in a server context.

Here is a simple example for brevity:

<input 
  placeholder={useTranslation("Type here")} // Requires server (context)
  onChange={() => { isDirty.value = true; }} // Requires client (island)
  />

The issue comes in two flavors,

  1. Different attributes for the same component requires different context.
  2. At any point an island is used in a DOM tree, all descendents can no longer reach the server context, components in /components/* has different meanings and limitations when rendered above and below the first island in the DOM tree.

Are there recommended ways to design an app structure around these issues?


EDIT: Some afterthoughts

Fresh seems to chop off at the first island and the whole subtree is passed to the client side.

This example of i18n means the context value can be too large to pass down the client, hence the rendering of descendents below the first island is still best done server side.

True server components must be rendered at server side even if they are the children of an island, the whole tree is then pieced back together at client side.

@deer
Copy link
Contributor

deer commented Feb 17, 2024

I created https://github.com/deer/fresh/tree/983_reproduction/tests/fixture_component_in_island to reproduce this situation. I have an index page which has an island, and the island has a component. So the server sends out:

<div id="index">
  Hello from the index. IS_BROWSER: false
  <!--frsh-island_default:0:-->
  <div id="island">
    Hello from an island. IS_BROWSER: false
    <div id="component">Hello from a component. IS_BROWSER: false</div>
  </div>
  <!--/frsh-island_default:0:-->
</div>

But what ends up getting rendered is:

<div id="index">
  Hello from the index. IS_BROWSER: false
  <div id="island">
    Hello from an island. IS_BROWSER: true
    <div id="component">Hello from a component. IS_BROWSER: true</div>
  </div>
</div>

@vicary, what you want is the following, right?

<div id="index">
  Hello from the index. IS_BROWSER: false
  <div id="island">
    Hello from an island. IS_BROWSER: true
    <div id="component">Hello from a component. IS_BROWSER: false</div>
  </div>
</div>

So whatever's rendered on the server for my poorly named <Component /> is currently re-rendered on the client (thus changing the value of IS_BROWSER, but you want it to somehow not be re-rendered.

To save some time, I'll include the three (boring) implementations here:

routes/index.tsx
import { IS_BROWSER } from "$fresh/runtime.ts";
import Island from "../islands/Island.tsx";

export default function Home() {
  console.log("index: " + IS_BROWSER);
  return (
    <div id="index">
      Hello from the index. IS_BROWSER: {IS_BROWSER === true ? "true" : "false"}
      <Island />
    </div>
  );
}
islands/Island.tsx
import { IS_BROWSER } from "$fresh/runtime.ts";
import Component from "../components/Component.tsx";

export default function Island() {
  console.log("island: " + IS_BROWSER);
  return (
    <div id="island">
      Hello from an island. IS_BROWSER: {IS_BROWSER === true ? "true" : "false"}
      <Component />
    </div>
  );
}
components/Component.tsx
import { IS_BROWSER } from "$fresh/runtime.ts";

export default function Component() {
  console.log("component: " + IS_BROWSER);
  return (
    <div id="component">
      Hello from a component. IS_BROWSER: {IS_BROWSER === true ? "true" : "false"}
    </div>
  );
}

@vicary
Copy link

vicary commented Feb 18, 2024

@deer Yes. And to leave no room for interpretation, I would add two changes to emphasize the transparency before and after the first island in the DOM tree.

  1. The same component should be reusable.
    <div id="index">
      Hello from the index. IS_BROWSER: false
    + <div id="component">Hello from a component. IS_BROWSER: false</div>
      <div id="island">
        Hello from an island. IS_BROWSER: true
        <div id="component">Hello from a component. IS_BROWSER: false</div>
      </div>
    </div>
  2. The two instances of component must share the same instance of <context.Provider> if one is provided.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants