denoland / fresh

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

Error: Converting circular structure to JSON at JSON.stringify - when context/reducer is used in island app folder #713

Closed og-dev closed 1 year ago

og-dev commented 2 years ago

I am using a simple Todo application to try Fresch App and I found this problem.

I have tried multiple ways to use Context with Reducer with an application, but it has been mission impossible. Either it doesn't work or it gives errors.

The component file that uses the context, I am creating it in the island folder.

When I create the context/reducer files in the island folder the application crashes:

TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)

If I put them outside the island folder, the application runs, but it is not possible to trigger the dispatch function to change the state and refresh the screen. Where should I create the context, provider and reducer files?

Is there or where can I find an example of how to use Context/Reducer in a Deno Fresh application with typescript?

constantgillet commented 2 years ago

I found this repo https://github.com/mandolyte/experiemnt-fresh/tree/main/use-context

maybe it will be usefull

EDIT: Okey I have tried and I have the same error than you

og-dev commented 2 years ago

I found this repo https://github.com/mandolyte/experiemnt-fresh/tree/main/use-context

maybe it will be usefull

EDIT: Okey I have tried and I have the same error than you

It is the same problem. In the example they use useState, I am using useReducer, but the result and problem is the same.

It seems that the failure is when the JavaScript code of the Context Provider is created.

If the provider is not in the island folder, the function to change the state and refresh the screen is not included in the JavaScript.

But if you put the Context Provider file in the island folder, you get the error of Converting circular structure to JSON at JSON.stringify

Thanks for finding that repo, so others can run it and maybe find the problem.

lucacasonato commented 2 years ago

It is only possible to pass JSON serializable values to as props to islands. Why? Because there are actually two renders going on. The first render happens on the server - here the route + all islands are rendered. Here any prop can be passed between route and island, because they are being rendered in the same JavaScript VM. Then you have the client side render. The client side render ONLY renders islands. There is no route that is rendered that could pass props to the island. To make the island receive the right props anyway, we need to capture the props passed to the island during the server render and then send them to the client. This sending is done using JSON serialization.

And why do contexts not work? Again it's related to the two renders. Contexts can work on the server render because there is one continuous render going on. But in the client side render the route is not rendered, only the islands. Because of this, during the island render any context defined in the route will be absent, because during the client render these context providers were never run.

lucacasonato commented 2 years ago

This is also elaborated in https://fresh.deno.dev/docs/concepts/islands.

og-dev commented 2 years ago

This is also elaborated in https://fresh.deno.dev/docs/concepts/islands.

Thanks for your excellent clarification and the reasons why Fresh works that way.

Under that concept, what would be the best way to pass data and update the state of one or several components if I can't pass a function using context/reducer?

I would like to use the context/reducer approach to avoid Prop Drilling.

Could you please do a suggestion or show an example how to do it using Fresh?

lucacasonato commented 2 years ago

Are you attempting to share state between multiple islands, or between the SSR'd route and the island?

og-dev commented 2 years ago

I need to share state:

From a child to parent component and from parent to child components.

<TabList active_tab=1> // Initial State TabItem 1 is active (highlighted)

  <TabItem id=1 icon="incomplete_data.png" onClick={showTabPanelContainer(1)} is_active={true}/>

   // onClick Show TabPanelContainer 2 and set this TabItem as active
   // now the highlighted and active tab must be this one (the clicked tab)
  <TabItem id=2 icon="incomplete_data.png" onClick={showTabPanelContainer(2)} is_active={false}/>

  <TabItem id=3 icon="incomplete_data.png" onClick={showTabPanelContainer(3)} is_active={false}/>
  <TabItem id=4 icon="incomplete_data.png" onClick={showTabPanelContainer(4)} is_active={false}/>

</TabList>

<TabPanelContainer>

  <TabPanelContent id=1 >...</TabPanelContent>

  <TabPanelContent id=2 >
     // If form data is complete change Tab Icon State
     // from incomplete_data.png to data_completed.png and refresh screen
     <FormData onChange={changeTabItemIcon(2)} />
  </TabPanelContent>

  <TabPanelContent id=3 >...</TabPanelContent>
  <TabPanelContent id=4 >...</TabPanelContent>

</TabPanelContainer>
lucacasonato commented 2 years ago

These are the rules you have to work within:

mistricky commented 1 year ago

I face the same problem, it sounds like the Fresh does not support preact@latest/signals yet, is there another solution?

mistricky commented 1 year ago

I can understand why difficult to share live states between route components and islands, but why also I can't share live states between islands? Cause should there does not have any problem about pass props, according to my understanding, islands have the same render count.

deer commented 1 year ago

Sadly there aren't any reproduction steps here, but it looks like there are two problems mentioned:

Is there anything else to do here? Maybe this can be closed. @lucacasonato

marvinhagemeister commented 1 year ago

Good point, yeah these PRs resolve this issue.