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

Introduce Suspense and lazy #1593

Merged
merged 37 commits into from May 9, 2019

Conversation

sventschui
Copy link
Member

@sventschui sventschui commented May 2, 2019

This introduces Suspense and lazy. There are some open things tough.

EDIT: After the excellent feedback on this PR things seem to be close to finish 🎉 Currently this PR adds 187 bytes gzipped to core.

Open things to do / disucss

  • Fix issue of Suspense inside Fragment messing up the order of the DOM (Fix incorrect DOM when Fragment has siblings #1605)
    • Skipping this test for now
  • Should Suspense / lazy remain part of the core or live in preact/suspense or preact/compat? This would save 137 bytes from core what would only add 50 bytes to core for somebody not using suspense
    • Keeping them in core for now. If you wish I can move them...
  • Shall we drop isSuspense = true inside catchErrorInComponent resulting in 3B saving but also make errors thrown inside _childDidSuspend result in a Missing Suspense error?
    • I'd not do so, marking as complete
  • Warn about propTypes on lazy() in preact/debug
  • Warn about missing Suspense in preact/debug
    • Not possible without some more changes. A Missing Suspense error must do the job for now
  • Golf some bytes
  • Create a demo

Copy link
Member

@JoviDeCroock JoviDeCroock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there's an ErrorBoundary between the lazy and Suspense component. Wanted to implement it like this aswell but that's the issue I kept bumping against.

Ideally those warnings are thrown in /debug but can be discussed later.
As to the testing strategy I'm not sure about that

@sventschui
Copy link
Member Author

sventschui commented May 2, 2019

We could add a prop (e.g. promise.Suspense = Symbol.for('Suspense')) to the promises thrown by lazy and then skip user-land componentDidCatch for these errors

We should also investigate how React behaves as Suspense is catching promises in React too.

EDIT: Just hacked together a fiddle to check the behaviour of React. Seems React doesn't send promises to user-land componentDidCatch at all. Even when throwing them in custom components. What React does is throwing an error if there is no Suspense in the tree above a component throwing a Promise: https://jsfiddle.net/sventschui/dovLt0em/6/

I'd propse to not send anything that has a then function to componentDidCatch. What is missing then is a way to throw an error if there is no suspense in the hierarchy. Maybe you can point me into the right direction to implement this?

@coveralls
Copy link

coveralls commented May 2, 2019

Coverage Status

Coverage remained the same at 100.0% when pulling 0431636 on sventschui:feature/suspense-lazy into b1a1956 on developit:master.

@sventschui
Copy link
Member Author

sventschui commented May 2, 2019

While writing the tests one thing came to my attention. When using Suspense in React the suspended component tree doesn't loose state. It isn't unmounted, just "parked". I'm not sure whether this is possible with the current solution. Do you have an idea how to handle this?

EDIT: Seems I was wrong on this. Components are re-created in react too (see https://jsfiddle.net/sventschui/3auzjo67/21/)

@JoviDeCroock
Copy link
Member

JoviDeCroock commented May 2, 2019

That would be something very specific to a two phase render cycle and is not possible at this time as far as I know.

I’m also not sure as to what point we want to support this in core since this adds a lot of bytes, it’s already adding 400 at this point.

Cc @developit @marvinhagemeister @andrewiggins

@sventschui
Copy link
Member Author

sventschui commented May 3, 2019

I guess most of the bytes come from a huge error message I copied from React. We should replace this with a shorter one or even with just a short-link to a wiki/error description page. Also we might not want to bundle in the Suspense and lazy components but rather have them in like preact/suspense to save this bytes for folks not using Suspense. Do you have a strategy in place for things like this?

The state keeping thing is quite important as it breaks all the tests and compat to React currently.

When not having suspense in core we at least should try to make it possible in a user-land extension. That means having catchErrorInComponent extendable/replaceable and something to make the state keeping possible. Honestly the dirties hack that comes to my mind here is keeping it as display hidden in user-land code but that will definitely introduce other issues.

For the state keeping we might want to try to flag the direct children of Suspend as suspended, keep them as a prop to Suspense and not call unmount on these. During the next render simply re-run render on these children. Thinking about this makes me think of implementing Suspense not as a component but as code in the core that gets triggered if a component is of type Symbol.for("Suspense")

Sorry for my thought flush on my phone. Will look into this a bit deeper today once I get my fingers to my macbook.

EDIT: React doesn't seem to keep state neither. I was wrong on this. (see https://jsfiddle.net/sventschui/3auzjo67/21/)


if (isSuspend && suspendingComponent) {
// TODO: what is the preact way to determine the component name?
const componentName = suspendingComponent.displayName || (suspendingComponent._vnode && suspendingComponent._vnode.type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this section into preact/debug. A Suspense component without a fallback doesn't seem to make much sense, so we could just always warn on that. This would also keep core small and tiny 🎉

EDIT: Removing that would save 158 B 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also allows the recheck of isSuspsend and suspendingComponent, so that will save a lot

Copy link
Member Author

@sventschui sventschui May 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still throw the Missing Suspense error on the suspendingComponent.

Would you want to save some bytes and just throw the error at the end of the function instead of the suspendingComponent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What he's saying is to throw it in a hook from preact/debug since it doesn't really matter in production anyway. I also think that is the best approach to maintain our lightweight profile

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you guys give me a hint on where I could add this to preact/debug. I did not find a proper place to add it

src/suspense.js Outdated Show resolved Hide resolved
src/suspense.js Outdated Show resolved Hide resolved
src/suspense.js Outdated Show resolved Hide resolved
src/suspense.js Outdated Show resolved Hide resolved
src/diff/index.js Outdated Show resolved Hide resolved
Copy link
Member

@marvinhagemeister marvinhagemeister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really cool start 👍 Left a few comments, let me know what you think 🙂

@sventschui
Copy link
Member Author

sventschui commented May 3, 2019

Thanks for these hints. Will definitely incorporate these once I get closer to a working implementation.

The thing that currently bothers me is keeping the state of the suspended component tree (as React does). I was thinking of doing the following:

Can't think of how to achieve the second point. One of you guys got an idea how to solve this?

EDIT: It seems I was wrong. React doesn't seem to keep state neither (see https://jsfiddle.net/sventschui/3auzjo67/21/)

@sventschui
Copy link
Member Author

I justed hacked a working solution together to keep state of suspended components 🎉 .

Would be cool if you could look into this. I don't really feel confident with the current solution. I'll invest some more time when I get to it.

Copy link
Member

@JoviDeCroock JoviDeCroock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First and forall, congratulations on the already -100B 💯

Looking like an awesome feature to have in all honesty.
Something that I'm thinking about is that it could actually flicker if we don't park DOM (we can test this in demo maybe)

@ForsakenHarmony
Copy link
Member

might want to have a test with multiple suspended components in one suspense

src/diff/index.js Outdated Show resolved Hide resolved
@LukasBombach
Copy link
Contributor

@sventschui @marvinhagemeister can you check if this implementation works with Next?

I have setup a monorepo project that uses this branch with next and a next plugin similar to @zeit/next-preact. Basically I have copy and pasted their code and used it with this branch. Check it out here:

https://github.com/spring-media/preact-next-suspense

I have written a bit of a documentation to explain the repo in the README

@sventschui
Copy link
Member Author

@LukasBombach as mentioned in slack: The error is due to an old version of preact-render-to-string. You need to update to >=5

component._childDidSuspend(error);
}
else {
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move to here?

Suggested change
continue;
return catchErrorInComponent(Error('Missing Suspense'), suspendingComponent);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The continue results in traversing up the tree. This would break the tree traversing AFAIK

@developit
Copy link
Member

developit commented May 6, 2019

Question: what's the reason for using ._childDidSuspend? It seems like the Suspense component could just implement componentDidCatch:

class Suspense {
  componentDidCatch(e) {
    if (e && e.then) e.then(() => this.setState({}));
    else throw e;
  }
}

This would mean that folks don't pay for Suspense implementation details if they don't import Suspense.

@sventschui
Copy link
Member Author

sventschui commented May 6, 2019

Well the naming of the method is irrelevant. It just needs to take precedence over regular componentDidCatch when the error thrown is a promise (even if between the throwing component and <Suspense> is another error boundary).

Edit: Did not yet check but if renaming saves bytes then I'll rename it

@LukasBombach
Copy link
Contributor

@LukasBombach as mentioned in slack: The error is due to an old version of preact-render-to-string. You need to update to >=5

👌

diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, c, oldDom);
// Passing the ancestorComponent instead of c here is needed for catchErrorInComponent
// to properly traverse upwards through fragments to find a parent Suspense
diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, oldDom);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! This may also fix an issue I was having when working on the devtools where Fragments would break an upwards traversal 👍

Copy link
Member

@marvinhagemeister marvinhagemeister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the journey of this PR 🎉 In my opinion it is ready to be merged. I'd just skip the test case with Fragments (via it.skip()) because this problem will be dealt with separately in #1605 🎉

Copy link
Member

@JoviDeCroock JoviDeCroock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated by @marvinhagemeister this PR is amazing! Great work on it, just one thing that needs to be added is a test that tests your error message in /debug then the coverall will report 100% again.

@LukasBombach
Copy link
Contributor

@sventschui @marvinhagemeister @JoviDeCroock I can confirm this PR works with Next 8.x using preact-render-to-string@next as @sventschui proposed:

spring-media/preact-next-suspense@5a7de52

}
}
}

// TODO: Add a react-like error message to preact/debug
Copy link
Contributor

@38elements 38elements May 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this Todo need to be resolved ?
If it is unnecessary, I think it is better to remove it.

Copy link
Member Author

@sventschui sventschui May 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to tackle this (see TODO in PR description) but currently don't know how to do this without adding too many bytes to core

@LukasBombach
Copy link
Contributor

LukasBombach commented May 7, 2019

Oh wait, there is still an issue with this and Next.js when you run my repo and open http://localhost:3000, you'll get an error in node saying

[ error ] TypeError: component.props.handleStateChange is not a function
    at emitChange (preact-next-suspense/node_modules/next-server/dist/lib/side-effect.js:11:29)
    at new SideEffect (preact-next-suspense/node_modules/next-server/dist/lib/side-effect.js:26:17)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1371)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:2849)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at s (preact-next-suspense/node_modules/preact-render-to-string/dist/index.js:1:1683)
    at render (preact-next-suspense/node_modules/next-server/dist/server/render.js:86:16)
    at renderPage (preact-next-suspense/node_modules/next-server/dist/server/render.js:211:20)

@sventschui
Copy link
Member Author

@LukasBombach I once encountered this issue too. As far as I remember it was caused by context not being set correctly but this should be fixed. I can run your example without any issues

@marvinhagemeister
Copy link
Member

marvinhagemeister commented May 7, 2019

@LukasBombach a new version for preact-render-to-string with support for creeateContext hasn't been published yet. The code is already in master but I was too busy the past days. The next release should be up by the end of the week.

EDIT: published it 🎉

@LukasBombach
Copy link
Contributor

@sventschui have you checked your node console after loading the index page in your browser?

@LukasBombach
Copy link
Contributor

LukasBombach commented May 8, 2019

EDIT: published it 🎉

@marvinhagemeister @sventschui that update seems to have fixed the error 🎉

PS: I have updated my repo for you to try out

Copy link
Member

@marvinhagemeister marvinhagemeister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wohoo! Let's do this👍🎉✔️

@marvinhagemeister marvinhagemeister merged commit a23b921 into preactjs:master May 9, 2019
@LukasBombach
Copy link
Contributor

🎉 😍

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

Successfully merging this pull request may close these issues.

None yet