CallumBoase / plasmic-supabase

GNU General Public License v3.0
8 stars 5 forks source link

plasmic-supabase does not currently support Static Site Generation and/or Incremental Static Regeneration out-of-the-box #6

Open CallumBoase opened 6 months ago

CallumBoase commented 6 months ago

Context

The Plasmic docs on data-fetching code components suggest using the function usePlasmicQueryData to wrap any data queries.

This enables data queries to be pre-rendered during render, so that your site can be pre-generated and/or cached with results of queries data using Static Site Generation and/or Incremental Static Regeneration.

When using plasmic to generate a nextjs app with the loader API (as opposed to codegen), ISR works by default if you've used usePlasmicQueryData to wrap data queries, due to the getStaticProps function using extractPlasmicQueryData (see this default catchall page).

Problem

All data queries in plasmic-supabase avoid use of usePlasmicQueryData, and instead use use-swr.

This was done because, despite usePlasmicQueryData being a lightweight wrapper around use-swr, it did not seem to support optimistic updates, which is a key functionality of plasmic-supabase.

Solution

Figure out how to fetch data in plasmic-supabase in a way supports both SSG / ISR, and optimistic updates. Ideally it should work out-of-the-box with plasmics default catchall page generated when you publish a Plasmic app to nextjs using the loader API, however it could be OK to provide clear instructions to users on how they adjust the catchall page to support SSG and/or ISR if needed.

CallumBoase commented 5 months ago

We'll need to deal with https://github.com/CallumBoase/plasmic-supabase/issues/7 in order to support SSG / ISR using plasmic-supabase components

CallumBoase commented 5 months ago

Discovered that useSWR() does actually populate the query cache once issue described in https://github.com/CallumBoase/plasmic-supabase/issues/7 is temporarily nullified.

Steps to reproduce:

  1. Create a branch of plasmic-supabase to run for local development
  2. In plasmic-supabase change ./utils/supabase/component.ts to remove all logic for fetching cookies or local storage. New contents:
    
    import { createBrowserClient } from '@supabase/ssr'

export default function createClient() { //Create the supabase client like normal const supabase = createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, );

return supabase;

};

(this obviously breaks the ability to fetch the session and run authenticated API calls to Supabase, it's just for testing)
3. run `npm run build` then `npm pack` to create a local tarball of your dev version
4. Create a plasmic project, and follow the basic installation instructions (publish to github, download repo to local machine, run npm install, add .env.local with your supabase credentials)
5. Install plasmic-supabase from your local tarball
6. Init the plasmic-supabase components, start local dev server, configure custom app host
7. From Plasmic studio: Add `SupabaseProvider` to your new app's home page using Plasmic user interface and configure it to pull data from a non-rls protected table. JSON.stringify the data from the SupabaseProvider onto the page into a text element.
8. in your test plasmic projects `[[...catchall]].tsx` file, inside `getStaticProps`, after `const queryCache = await extractPlasmicQueryData....` add this line
```typescript
console.log(JSON.stringify(queryCache))
  1. Access localhost:3000 to view your locally running app. in terminal, you will NOT get any errors from react-ssr-prepass in your dev server terminal (ie you won't get the errors described https://github.com/CallumBoase/plasmic-supabase/issues/7). This is because we aren't using window or document objects when Plasmic runs react-ssr-prepass during getStaticProps(), so the data fetch works on the server. You will see in terminal a console.log of the query cache from when getStaticProps ran. You should see the data that you fetch using the SupabaseProvider in here, even though it wasn't fetched inside usePlasmicQueryData. However, you will see a hydration error in the browser (cause unknown at this stage)
CallumBoase commented 5 months ago

Troubleshooting previous comment, why we get hydration error on pages with a SupabaseProvider even when you've adjusted plasmic-supabase/utils/supabase/component.ts to not use window or cookies and therefore not cause react-ssr-prepass to error:

Info 1: tried transitioning a development version of plasmic-supabase component SupabaseProvider to use useMutablePlasmicQueryData in place of useSWR and using that in a test project. Still get hydration errors

Info 2:

In a test plasmic project (with or without plasmic-supabase installed) create a new code component with the basics of fetching similar to SupabaseProvider

import { useMutablePlasmicQueryData } from "@plasmicapp/query";
import { DataProvider } from "@plasmicapp/loader-nextjs";

import {createBrowserClient as createClient} from '@supabase/ssr'

export function SimpleSupabase({ children }: { children: React.ReactNode }) {
  const { data } = useMutablePlasmicQueryData('/simpleSupabase', async () => {
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    );
    const { data, error } = await supabase.from('country').select('*');
    //random num between 1 and 19
    const randomNum = Math.floor(Math.random() * 19) + 1;
    //simulate delay
    await new Promise(resolve => setTimeout(resolve, 3000));
    if(error) throw error;
    return data[randomNum];
  });

  return (
    <>
      {data && (
        <DataProvider name="SimpleSupabase" data={data}>
          {children}
        </DataProvider>
      )}
    </>
  );
}

Then register that in plasmic-init.ts

PLASMIC.registerComponent(SimpleSupabase, {
  name: "SimpleSupabase",
  providesData: true,
  props: {
    children: "slot"
  }
})

This works as expected! Data is cached on the server, and 3 seconds later you see the stale data revalidated with a new random country

Info 3: Using useSWR in place of useMutablePlasmicQueryData does not work. Data is not fetched during server side render

import useSWR from "swr";
import { DataProvider } from "@plasmicapp/loader-nextjs";

import {createBrowserClient as createClient} from '@supabase/ssr'

export function SimpleSupabase({ children }: { children: React.ReactNode }) {
  const { data } = useSWR('/simpleSupabase', async () => {
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    );
    const { data, error } = await supabase.from('country').select('*');
    //random num between 1 and 19
    const randomNum = Math.floor(Math.random() * 19) + 1;
    //simulate delay
    await new Promise(resolve => setTimeout(resolve, 3000));
    if(error) throw error;
    return data[randomNum];
  });

  return (
    <>
      {data && (
        <DataProvider name="SimpleSupabase" data={data}>
          {children}
        </DataProvider>
      )}
    </>
  );
}

Next steps:

  1. See if SupabaseProvider works fully with useMutablePlasmicQueryData including optimistic updates
  2. Starting with simple component, gradually add back functionality that is present in SupabaseProvider until hydration error is encountered?
CallumBoase commented 5 months ago

Gradually adding functionality from SupabaseProvider into SimpleSupabase component to identify where hydration errors occur & to check if all behaviour works

dynamic query name: **works fine** ```typescript //SimpleSupabase.tsx import { useMutablePlasmicQueryData } from "@plasmicapp/query"; import { DataProvider } from "@plasmicapp/loader-nextjs"; import {createBrowserClient as createClient} from '@supabase/ssr' export function SimpleSupabase({ children, queryName }: { children: React.ReactNode, queryName: string }) { const { data } = useMutablePlasmicQueryData(queryName, async () => { const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); const { data, error } = await supabase.from('country').select('*'); //random num between 1 and 19 const randomNum = Math.floor(Math.random() * 19) + 1; //simulate delay await new Promise(resolve => setTimeout(resolve, 3000)); if(error) throw error; return data[randomNum]; }); return ( <> {data && ( {children} )} ); } ``` ```typescript //plasmic-init.ts PLASMIC.registerComponent(SimpleSupabase, { name: "SimpleSupabase", providesData: true, props: { children: "slot", queryName: 'string' } }) ```
Add row element action with optimistic: works! ```typescript //SimpleSupabase.ts import { useMutablePlasmicQueryData } from "@plasmicapp/query"; import { DataProvider } from "@plasmicapp/loader-nextjs"; import { createBrowserClient } from "@supabase/ssr"; import { forwardRef, useCallback, useImperativeHandle } from "react"; import { RunningCodeInNewContextOptions } from "vm"; interface Actions { addRow(rowForSupabase: any, optimisticRow: any): void; } type Row = { [key: string]: any; }; type Rows = Row[] | null; interface SimpleSupabaseProps { children: React.ReactNode; queryName: string; } //An unrealistically simplified createClient without ref to cookies or local storage const createClient = () => createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); export const SimpleSupabase = forwardRef( function SimpleSupabase(props, ref) { const { children, queryName } = props; const { data, mutate } = useMutablePlasmicQueryData(queryName, async () => { const supabase = createClient(); const { data, error } = await supabase.from("test_pub").select("*"); //random num between 1 and 10 // const randomNum = Math.floor(Math.random() * 10) + 1; //simulate delay await new Promise((resolve) => setTimeout(resolve, 3000)); if (error) throw error; return data; }); const addRowOptimistically = useCallback( (data: Rows | null, optimisticRow: Row) => { const newData = [...(data || []), optimisticRow]; return newData; }, [] ); const addRow = useCallback( async ( rowForSupabase: Row, optimisticRow: Row, optimisticFunc: (data: Rows, optimisticData: any) => Rows ) => { //Add the row to supabase const supabase = createClient(); const { error } = await supabase.from('test_pub').insert(rowForSupabase); if (error) throw error; return optimisticFunc(data, optimisticRow); }, [data] ); //Function that just returns the data unchanged //To pass in as an optimistic update function when no optimistic update is desired //Effectively disabling optimistic updates for the operation function returnUnchangedData(data: Rows) { return data; } //Helper function to choose the correct optimistic data function to run function chooseOptimisticFunc( optimisticOperation: string | null | undefined, elementActionName: string ) { if (optimisticOperation === "addRow") { return addRowOptimistically; // } else if (optimisticOperation === "editRow") { // return editRowOptimistically; // } else if (optimisticOperation === "deleteRow") { // return deleteRowOptimistically; // } else if (optimisticOperation === "replaceData") { // return replaceDataOptimistically; } else { //None of the above, but something was specified if (optimisticOperation) { throw new Error(` Invalid optimistic operation specified in "${elementActionName}" element action. You specified "${optimisticOperation}" but the allowed values are "addRow", "editRow", "deleteRow", "replaceData" or left blank for no optimistic operation. `); } //Nothing specified, function that does not change data (ie no optimistic operation) return returnUnchangedData; } } useImperativeHandle(ref, () => ({ //Element action to add a row with optional optimistic update & auto-refetch when done addRow: async (rowForSupabase, optimisticRow) => { //Choose the optimistic function based on whether the user has specified optimisticRow //No optimisticRow means the returnUnchangedData func will be used, disabling optimistic update let optimisticOperation = optimisticRow ? "addRow" : null; const optimisticFunc = chooseOptimisticFunc( optimisticOperation, "Add Row" ); //Run the mutation mutate(addRow(rowForSupabase, optimisticRow, optimisticFunc), { populateCache: true, optimisticData: optimisticFunc(data, optimisticRow), }).catch((err) => console.error(err)); }, })); return ( <> {data && ( {children} )} ); } ); ``` ```typescript //plasmic-init.ts PLASMIC.registerComponent(SimpleSupabase, { name: "SimpleSupabase", providesData: true, props: { children: "slot", queryName: 'string' }, refActions: { addRow: { description: 'Add a row', argTypes: [ {name: 'rowForSupabase', type: 'object'}, {name: 'optimisticRow', type: 'object'} ] } } }) ```
Surrounding `DataProvider` in a div component with classname - works fine ```typescript //SimpleSupabase.tsx import { useMutablePlasmicQueryData } from "@plasmicapp/query"; import { DataProvider } from "@plasmicapp/loader-nextjs"; import { createBrowserClient } from "@supabase/ssr"; import { forwardRef, useCallback, useImperativeHandle } from "react"; import { RunningCodeInNewContextOptions } from "vm"; interface Actions { addRow(rowForSupabase: any, optimisticRow: any): void; } type Row = { [key: string]: any; }; type Rows = Row[] | null; interface SimpleSupabaseProps { children: React.ReactNode; queryName: string; className?: string; } //An unrealistically simplified createClient without ref to cookies or local storage const createClient = () => createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); export const SimpleSupabase = forwardRef( function SimpleSupabase(props, ref) { const { children, queryName, className } = props; const { data, mutate } = useMutablePlasmicQueryData(queryName, async () => { const supabase = createClient(); const { data, error } = await supabase.from("test_pub").select("*"); //random num between 1 and 10 // const randomNum = Math.floor(Math.random() * 10) + 1; //simulate delay await new Promise((resolve) => setTimeout(resolve, 3000)); if (error) throw error; return data; }); const addRowOptimistically = useCallback( (data: Rows | null, optimisticRow: Row) => { const newData = [...(data || []), optimisticRow]; return newData; }, [] ); const addRow = useCallback( async ( rowForSupabase: Row, optimisticRow: Row, optimisticFunc: (data: Rows, optimisticData: any) => Rows ) => { //Add the row to supabase const supabase = createClient(); const { error } = await supabase.from('test_pub').insert(rowForSupabase); if (error) throw error; return optimisticFunc(data, optimisticRow); }, [data] ); //Function that just returns the data unchanged //To pass in as an optimistic update function when no optimistic update is desired //Effectively disabling optimistic updates for the operation function returnUnchangedData(data: Rows) { return data; } //Helper function to choose the correct optimistic data function to run function chooseOptimisticFunc( optimisticOperation: string | null | undefined, elementActionName: string ) { if (optimisticOperation === "addRow") { return addRowOptimistically; // } else if (optimisticOperation === "editRow") { // return editRowOptimistically; // } else if (optimisticOperation === "deleteRow") { // return deleteRowOptimistically; // } else if (optimisticOperation === "replaceData") { // return replaceDataOptimistically; } else { //None of the above, but something was specified if (optimisticOperation) { throw new Error(` Invalid optimistic operation specified in "${elementActionName}" element action. You specified "${optimisticOperation}" but the allowed values are "addRow", "editRow", "deleteRow", "replaceData" or left blank for no optimistic operation. `); } //Nothing specified, function that does not change data (ie no optimistic operation) return returnUnchangedData; } } useImperativeHandle(ref, () => ({ //Element action to add a row with optional optimistic update & auto-refetch when done addRow: async (rowForSupabase, optimisticRow) => { //Choose the optimistic function based on whether the user has specified optimisticRow //No optimisticRow means the returnUnchangedData func will be used, disabling optimistic update let optimisticOperation = optimisticRow ? "addRow" : null; const optimisticFunc = chooseOptimisticFunc( optimisticOperation, "Add Row" ); //Run the mutation mutate(addRow(rowForSupabase, optimisticRow, optimisticFunc), { populateCache: true, optimisticData: optimisticFunc(data, optimisticRow), }).catch((err) => console.error(err)); }, })); return ( <> {data && (
{children}
)} ); } ); ``` ```typescript //plasmic-init.ts PLASMIC.registerComponent(SimpleSupabase, { name: "SimpleSupabase", providesData: true, props: { children: "slot", queryName: 'string' }, refActions: { addRow: { description: 'Add a row', argTypes: [ {name: 'rowForSupabase', type: 'object'}, {name: 'optimisticRow', type: 'object'} ] } } }) ```

Important note: optimistic operations work! This means we should be able to transition the product PlasmicSupabase to use useMutablePlasmicQueryData

Next steps: continue adding parts of PlasmicSupabase component until hydration error is found.

CallumBoase commented 5 months ago

Copy-pasted SupabaseProvider and all it's dependencies from plasmic-supabase into a sample app

transitioned createClient() inside SupabaseProvider to the simplified one

import { createBrowserClient} from '@supabase/ssr';

//An unrealistically simplified createClient without ref to cookies or local storage
const createClient = () => createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

And transitioned useSWR in SupabaseProvider into useMutablePlasmicQueryData

When running the example app with pages containing non-rls protected api calls to Supabase via SupabaseProvider, it seems to support server side data prefetching and caching as expected, and there's no hydration errors!

(exact setup here).

Next steps: try that exact setup in the plasmic-supabase repo instead and see if something about having plasmic-supabase as an external dependency is causing the hydration issue

Edit: the simple SupabaseProvider directly used DOES seem to be working with SSG and ISR But making equivalent changes and either importing SupabaseProvider from install, or a very similar local version, neither seem to do ISR or SSG - the server html always says No data and validating (I think).

Need to confirm this behaviour

CallumBoase commented 5 months ago

This direct usage of SupabaseProvider directly from the project (as opposed to installed from plasmic-supabase) works

The exact same SupabaseProvider installed from local dev tgz of Plasmic-Supabase (external dependency) DOES fail for some reason! We get hydration errors again

CallumBoase commented 5 months ago

Trying to bundle plasmic-supabase using more complete bundling with Vite instead of basic typescript bundling to see if that fixes the hydration error.

Initial attempt here: https://github.com/CallumBoase/plasmic-supabase-new-bundling-test And used in this branch: https://github.com/CallumBoase/Plasmic-supabase-test-130624-0945hrs/tree/plasmic-supabase-new-bundling-test

Result from initial test: no hydration errors, but the data is not available in react-ssr-prepass and the component is not server rendered either!

CallumBoase commented 5 months ago

Tried bundling with same configuration as @plasmicapp/pkgs/sanity-io

It worked with SupabaseProvider: https://github.com/CallumBoase/plasmic-supabase-bundled-like-plasmic including

Next steps: