Giter VIP home page Giter VIP logo

preact-ssr-prepass's Introduction

preact-ssr-prepass

npm Coverage Status OpenCollective Backers OpenCollective Sponsors travis

Drop-in replacement for react-ssr-prepass.

Neither Preact nor React support Suspense on the server as of now. Heavily inspired by react-ssr-prepass, preact-ssr-prepass provides a two-pass approach with which Suspense can be used on the server. In the first pass, preact-ssr-prepass will create a VNode tree and await all suspensions, in the second pass preact-render-to-string can be used to render a vnode to a string.

Even if preact-ssr-prepass is designed to do as little as possible, it still adds a slight overhead since the VNode tree is created twice.

⚠️ Note that this is neither an official Preact nor React API and that the way Suspense is handled on the server might/will change in the future!

Usage / API

Awaiting suspensions

preact-ssr-prepass needs to be called just before rendering a vnode to a string. See the following example:

lazy.js:

export default function LazyLoaded() {
    return <div>I shall be loaded and rendered on the server</div>
}

index.js:

import { createElement as h } from 'preact';
import { Suspense, lazy } from 'preact/compat';
import renderToString from 'preact-render-to-string';
import prepass from 'preact-ssr-prepass';

const LazyComponent = lazy(() => import('./lazy'));

const vnode = (
    <Suspense fallback={<div>I shall not be rendered on the server</div>}>
        <LazyComponent />
    </Suspense>
);

prepass(vnode)
    .then(() => {
        // <div>I shall be loaded and rendered on the server</div>
        console.log(renderToString(vnode));
    });

Custom suspensions/data fetching using the visitor

preact-ssr-prepass accepts a second argument that allows you to suspend on arbitrary elements:

ssrPrepass(<App />, (element, instance) => {
  if (instance !== undefined && typeof instance.fetchData === 'function') {
    return instance.fetchData()
  }
});

API

/**
 * Visitor function to suspend on certain elements.
 * 
 * When this function returns a Promise it is awaited before the vnode will be rendered.
 */
type Visitor = (element: preact.VNode, instance: ?preact.Component) => ?Promise<any>;

/**
 * The default export of preact-ssr-prepass
 *
 * @param{vnode} preact.VNode The vnode to traverse
 * @param{visitor} ?Visitor A function that is called for each vnode and might return a Promise to suspend.
 * @param{context} ?Object Initial context to be used when traversing the vnode tree
 * @return Promise<any> Promise that will complete once the complete vnode tree is traversed. Note that even if
 *         a Suspension throws the returned promise will resolve.
 */
export default function prepass(vnode: preact.VNode, visitor?: Visitor, context:? Object): Promise<any>;

Replace react-ssr-prepass (e.g. next.js)

react-ssr-prepass is usually used on the server only and not bundled into your bundles but rather required through Node.js. To alias react-ssr-prepass to preact-ssr-prepass we recommend to use module-alias:

Create a file named alias.js:

const moduleAlias = require('module-alias')

module.exports = () => {
  moduleAlias.addAlias('react-ssr-prepass', 'preact-ssr-prepass')
}

Require and execute the exported function in your applications entrypoint (before require'ing react-ssr-prepass):

require('./alias')();

Differences to react-ssr-prepass

The visitor passed to preact-ssr-prepass gets a Preact element instead of a React one. When you use preact/compat's createElement it will make the element/vnode look as similar to a React element as possible.

preact-ssr-prepass's People

Contributors

aleksandrjet avatar dependabot[bot] avatar developit avatar dios-david avatar followdarko avatar jovidecroock avatar robertknight avatar rschristian avatar sventschui avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

preact-ssr-prepass's Issues

useId support

The latest version of preact added a much appreciated useId hook.

When using preact-ssr-prepass, preact-render-to-string, and useId in a typical SSR setup as outlined in the docs, preact throws this error within the useId function.

TypeError: Cannot read properties of undefined (reading 'length')

Looking into the source, it seems that the mangled _mask property on the vnode (__v ?) is undefined.

function b() {
	var n = p(r++, 11);
	return (
		n.__ ||
			(n.__ =
				'P' +
				(function (n) {
                                          console.log({n, u: u.__v}); // n is undefined
					for (var t = 0, r = n.length; r > 0; )
						t = ((t << 5) - t + n.charCodeAt(--r)) | 0;
					return t;
				})(u.__v.o) +
				r),
		n.__
	);
}

Is this expected?


Dependencies:

"@preact/signals": "^1.0.4",
"preact": "^10.11.0",
"preact-render-to-string": "^5.2.4",
"preact-ssr-prepass": "^1.2.0"

Ref gets lost during prepass?

I'm not sure whether this is a preact-ssr-prepass or a Preact bug, but I report here first :)

I'm migrating a project from React to Preact and I noticed a weird difference in prepass during SSR. I know Preact is not guaranteed to be fully compatible with React, so maybe this is not a bug at all :)

But looks like ref values get lost when using Suspense on SSR. Take this component as an example:

const App = () => {
  const initialised = useRef(false);

  console.log("initialised", initialised.current);

  if (!initialised.current) {
    initialised.current = true;
  }

  const [{ data }] = useQuery({
    query: `
      {
        country(code: "HU") {
          name
        }
      }
    `
  });

  return <span>{data.country.name}</span>;
};

What happens here is when the component is getting rendered in prepass:

  1. Component sets initialised ref to true (it's false by default)
  2. useQuery uses Suspense, so it throws a promise
  3. Once the promise is resolved, the component gets rendered again
  4. initialised ref value got lost, it's false again (with React it's still true)

To Reproduce with Preact

Sandbox with Preact: https://codesandbox.io/s/preact-ref-bug-preact-lswel?file=/src/App.js

  1. Go to http://localhost:3000
  2. Check console in the node app

The output is:

prepass start
initialised false
initialised false
prepass end

Expected behaviour with React

Sandbox with React: https://codesandbox.io/s/preact-ref-bug-react-h6ukj?file=/src/App.js

  1. Go to http://localhost:3000
  2. Check console in the node app

The output is:

prepass start
initialised false
initialised true
prepass end

Let me know if you need more details!

Does not work with hooks and preact/debug

I was trying to use react-router, preact and preact-ssr-prepass together and ran into some issues. After eliminating an unrelated ESM/CJS error, I discovered that I was still running into issues with preact-ssr-prepass. It turns out it can be reproduced with a pretty short example:

import 'preact/debug'; // if you comment this line out, it will work
import { h } from 'preact';
import renderToString from 'preact-render-to-string';
import prepass from 'preact-ssr-prepass';

import { useState } from 'preact/hooks';

const App = () => {
  const [x, setX] = useState(10);
  console.log(`Use state inside of App`, x);
  return h('div', undefined, x);
};

const vnode = h(App);

console.log('begin prepass');
await prepass(vnode);
console.log('end prepass');

const out = renderToString(vnode);

console.log(out);

Output:

❯ node index.js
begin prepass
Error: Hook can only be invoked from render methods.
    at async ESMLoader.import (https://node-qpdrhr-230n5kih.w.staticblitz.com/blitz.f2b7d4e326e0543f39833cc6d890b02bb01d7899.js:6:1209283)
    at async i.loadESM (https://node-qpdrhr-230n5kih.w.staticblitz.com/blitz.f2b7d4e326e0543f39833cc6d890b02bb01d7899.js:6:246622)
    at async handleMainPromise (https://node-qpdrhr-230n5kih.w.staticblitz.com/blitz.f2b7d4e326e0543f39833cc6d890b02bb01d7899.js:6:989292)

Here is a stackblitz: https://stackblitz.com/edit/node-qpdrhr?file=index.js


Basically as far as I can tell preact/debug is throwing an error because when the useState() hook is called, hooksAllowed is false. I guess this is related to the timing of the component lifecycle. I think the error from preact/debug seems mistaken because when I comment out that preact/debug import any code I write seems to work fine.

I wonder if this will be fixed by #47? Because it seems that options._diff function will set hooksAllowed = true.

The obvious solution may be to "not use preact/debug on the server side", which I think makes sense. But, in my case I was trying to set up an SSR project with @preact/preset-vite, which has the preact/debug hardcoded in so I never had a choice or knew it was being imported. I'm going to see if I can override it and skip the preact/debug import for the server side render.

`render` throws `Promise` for parameterized lazy components

I've been experimenting with Preact SSR and Suspense and tried to use preact-ssr-prepass but found render would always throw a Promise. I minified my test case and discovered this has to do with precise usage of lazy. I have a complete minimal repro here.

Essentially, this repo makes three attempts at defining a lazy component evaluated with Suspense. The first attempt looks like this:

function LazyComponent(): VNode {
    return <div>Hello, World!</div>;
}

const Comp = lazy(async () => LazyComponent);

// render as `<Suspense fallback={undefined}><Comp /></Suspense>`.

This attempt renders as you would expect, but also is kind of unnecessary. Let's get a little more complex with attempt 2.

function LazyComponent2(): VNode {
    const Comp2 = lazy(async () => {
        return () => <span>Hello, World!</span>;
    });

    return <Comp2 />;
}

// Render as `<Suspense fallback={undefined}><LazyComponent2 /></Suspense>`.

In this attempt we've moved the lazy call inside the component function to provide a bit more encapsulation. This attempt fails at render time and throws a Promise object directly with no error message. Not sure exactly what's wrong with this pattern, but clearly putting lazy inside the function breaks it. Maybe lazy can't be called at render time?

Let's try attempt 3, which is really just a justification for why you'd want to do this in the first place:

function ParameterizedLazyComponent({ id }: { id: number }): VNode {
    const Comp3 = lazy(async () => {
        const name = await getNameById(id); // Call an async function with a prop value.
        return () => <div>Hello, {name}!</div>;
    });

    return <Comp3 />;
}

// Does some async work, exactly what is not important here.
async function getNameById(id: number): Promise<string> {
    await new Promise<void>((resolve) => {
        setTimeout(resolve, 100);
    });

    return `Name #${id}`;
}

// Render as `<Suspense fallback={undefined}><ParameterizedLazyComponent id={1} /></Suspense>`.

This is the same as attempt 2, except it actually does some meaningful async work. This also fails with the same thrown Promise. Ultimately this is really what I want to do, invoke an async operation with a parameter which comes from a function prop. The only way I can see of to do this is to move the lazy call inside the component so it closes over the prop. However this pattern appears to break preact-ssr-prepass. lazy doesn't appear to provide any arguments to its callback, so I don't see any other obvious way of getting component prop data into the async operation.

I am new to Preact (and not terribly familiar with the React ecosystem in general) so apologies if this has a well-known answer. This feels like a rendering bug given that it is throwing a Promise without any error message. If there's a different pattern for developing parameterized lazy components, please let me know. As it stands, I don't see an obvious workaround here which does what I need it to do.

Prepass fails when running into a useLayoutEffect hooks

I've created a fork with a failing test: https://github.com/BartWaardenburg/preact-ssr-prepass

I’m running into issues when using preact-ssr-prepass since it fails in combination with preact/hooks (most likely the useLayoutEffect hook).

I’m getting the following error:

Server Error
TypeError: Cannot read property 'push' of undefined
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack
x
file:///Users/bartwaardenburg/Sites/ANWB/poncho-next/node_modules/preact/hooks/dist/hooks.js (1:543)
y.Image
webpack-internal:///./node_modules/@anwb/image/src/Image.js (67:71)
l
file:///Users/bartwaardenburg/Sites/ANWB/poncho-next/node_modules/preact-ssr-prepass/dist/index.js (1:968)

I am able to 'fix' this by doing the following change inside the preact/hooks dist:

- t.__h.push(i);
+ t.__H.__h.push(i);

Which sources inside the useLayoutEffect hook (_renderCallbacks is undefined) and I think the __H is the mangled .hooks property:

export function useLayoutEffect(callback, args) {
  /** @type {import('./internal').EffectHookState} */
  const state = getHookState(currentIndex++, 4);
  if (!options._skipEffects && argsChanged(state._args, args)) {
    state._value = callback;
    state._args = args;
    currentComponent._renderCallbacks.push(state);
  }
}

Is there any way we can skip the useLayoutEffects?

Prepass is not resolving lazy imports

I'm able to get through the pre-pass stage, but at the render stage, I get pending promise error.

So it looks like lazy imports are not resolved by pre-pass.

I have a boilerplate (the pre-pass error is in the branch prepass-ssr-issue):
https://github.com/r-k-t-a/starter/tree/prepass-ssr-issue

Prepass use:
https://github.com/r-k-t-a/starter/blob/prepass-ssr-issue/src/koa/routes/defaultRoute/defaultRoute.tsx#L40

Lazy imports:
https://github.com/r-k-t-a/starter/blob/prepass-ssr-issue/src/App.tsx

useId does not work

When using preact-ssr-prepass, preact-render-to-string, and useId, throws the following error:

TypeError: Cannot read properties of undefined (reading '__m')

I believe that this change may have affected.

preactjs/preact@062d62a

Release patch with fix exports field

Could you please release a patch to fix the export field in package.json? Unfortunately, this package cannot be used with the settings in tsconfig.json: module: "Node16", ModuleResolution: "Node16"

It was fixed here #56

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.