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

Adds support for server side suspense and streaming #923

Closed

Conversation

fivethreeo
Copy link
Contributor

Summary

React 18 now supports server side suspense and streaming.

This pull updates the babel plugin to transform lazy, makes the component suspend server-side and adds two methods to ChunkExtractor to incrementally write tags for streaming rendering.

Test plan

Run the example app at

https://github.com/fivethreeo/loadable-components/tree/feature/server-side-suspense/examples/streaming-server-side-rendering)

}

// `lazy()`
return (path.get('callee').isIdentifier({ name: 'lazy' }))
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍
Sounds like a bug. It would be great to get this change as a separate PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to add some snapshot tests, how do I update snapshots?

Copy link
Collaborator

Choose a reason for hiding this comment

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

jest -I, or just delete old ones before running the test

@theKashey
Copy link
Collaborator

Great work. But as it changes behavior and extends public API - details has to be discussed.

@fivethreeo
Copy link
Contributor Author

The code actually does not suspend server-side. Was a bug that made it work. So loads sync on server.

@fivethreeo
Copy link
Contributor Author

Will redo another way

@fivethreeo
Copy link
Contributor Author

There, should work as advertised with your tips now :)

@fivethreeo
Copy link
Contributor Author

Not quite right yet, ssg false seems not to work for some reason.

@theKashey
Copy link
Collaborator

Sound like it’s a good time to extend loadable testing capability in order to handle new scenarios. That always was a little painful to manual test every change as many important scenarios were not covered, and with the upcoming update that would became too much and too easy to break.

this is a task for another PR and separate stream of work

@fivethreeo
Copy link
Contributor Author

Was thinking I could try setting up some tests with react-testing-library. Will try to do a pr if that is of interest.

@theKashey
Copy link
Collaborator

I was thinking about a little more complex webpack based build, but the "complicated" part is why it still not around.
If you can create simpler RTL based solution which can test not "everything", but still "a lot" - that would be perfect.

@fivethreeo
Copy link
Contributor Author

Will make a new issue for testing to move this discussion there #930

@dglozic
Copy link

dglozic commented Dec 24, 2022

For my education: you added 'flush' variants for LinkTags and ScriptTags, but not for StyleTags. Are they not something that is dynamically discovered during rendering?

Also, to confirm: with these modifications, we can continue to use script preloading in head?

@fivethreeo
Copy link
Contributor Author

For my education: you added 'flush' variants for LinkTags and ScriptTags, but not for StyleTags. Are they not something that is dynamically discovered during rendering?

Also, to confirm: with these modifications, we can continue to use script preloading in head?

FlushStyleTags should be added.

According to reactwg/react-18#114

You should not use preload.

@fivethreeo
Copy link
Contributor Author

Just needs tests to confirm that the solution works as advertised.

@dglozic
Copy link

dglozic commented Dec 24, 2022

I have seen that, but if so, why are we still collecting link tags? What will those links be for?

@fivethreeo
Copy link
Contributor Author

I think link tags can be used with preload, if I understand correctly css and js will be loaded in order.

@dglozic
Copy link

dglozic commented Dec 27, 2022

Well, as I understand, we should no longer emit link tags for script preloading, and provide script tags as bootstrap scripts in 'renderToPipeableStream' setup.

I am eagerly awaiting a version of Loadable that supports 18 to test these in practice :-).

@fivethreeo
Copy link
Contributor Author

I think, don't use preload / prefetch In webpack. But load css with preload in link tags so script and css load in order. Need to check out what the browser does.

@dglozic
Copy link

dglozic commented Dec 27, 2022

Ah, OK. So compared to the current 16.8+ code, Loadable.Server will return the same styles, links and script tags, only we would need to keep adding them as they are discovered during streaming (if I understood your 'flush' additions).

@fivethreeo
Copy link
Contributor Author

Correct :)

@fivethreeo
Copy link
Contributor Author

https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types

So maybe fetchpriority "high" and rel "preload stylesheet"

@fivethreeo
Copy link
Contributor Author

Loadable needs testing before this can be verified to work. Need to set up e2e testing with cypress or playwright for the example.

@fivethreeo
Copy link
Contributor Author

You could always do a patch package if you want to use it now, but no guarantees.

@dglozic
Copy link

dglozic commented Jan 7, 2023

Thanks, I will wait for the official version I can just NPM install.

@theKashey
Copy link
Collaborator

narrator voice: 🐌 xmass break is a season of waiting 🍹

@fivethreeo
Copy link
Contributor Author

#944

@dglozic
Copy link

dglozic commented Jan 13, 2023

@fivethreeo reminder that we need 'Elements' variants of 'Tags' for 'flushX' (flushStyleElements, flushLinkElements, flushScriptElements).

@fivethreeo
Copy link
Contributor Author

@dglozic I am not sure that is needed, because flushing makes little sense when you are rendering instead of streaming html. When rendering you get all html in one go anyway.

@dglozic
Copy link

dglozic commented Jan 13, 2023

Actually yes, sorry, that was silly. When you flush, it goes directly into the stream and you don't really need Elements. Ignore :-).

@fivethreeo
Copy link
Contributor Author

Been quite sick this week so no time for anything but recuperating. But it is on my radar after I get the e2e testing working since that is needed to verify that this works as it should. So any help on what to test and how to test would help speed it along. I have some ideas ruminating but have not quite figured it out yet. Look at the wip pull for e2e testing.

@theKashey theKashey mentioned this pull request Jan 27, 2023
@dglozic
Copy link

dglozic commented Jan 27, 2023

Sorry to hear that, hope you get well soon!

I like @theKashey suggestion to bump the dependencies to React 18 right away, allowing teams to port to React 18 without making any changes to SSR and the use of Loadable. Once we stabilize, we can try out pre-release versions of loadable-components with support for 'flushXYZ'. It would limit the number of variables to watch for.

It is not as if React 18 has support for data fetching in Suspense anyway :-) - the API for that is not ready (only 'opinionated frameworks' like Next.js and Remix are experimenting with their own flavors).

@dglozic
Copy link

dglozic commented Jan 29, 2023

So now that dependencies were bumped and I was able to play with React 18.2, I am confused with extending the Writable. Right now, I am not yet doing full streaming: I am using renderToString to get the App content and collect the loadable elements (using extractor). Then I am wrapping the app content in JSX for html/head/body and other boilerplate, and streaming the entire thing using renderToPipeableStream. That works fine.

Just for kicks, I replaced res in pipe with the extended Writable as in the example. I am actually not doing anything in it, like this:

	const writable = new Writable({
		write(chunk, encoding, cb) {
			res.write(chunk, encoding, cb);
		},
		final(cb) {
			res.end();
			cb();
		},
		flush() {
			if (typeof res.flush === 'function') {
				res.flush();
			}
		},
		destroy: (err) => {
			res.destroy(err ?? undefined);
		}
	});

	const { pipe } = renderToPipeableStream(rootElement, {
		onShellReady: () => {
			pipe(writable);
		},
		onAllReady: () => {
			writable.write("");
		},
		onError: (err) => {
			_handleError(err, res, callback);
		},
		onShellError: (err) => {
			_handleError(err, res, callback);
		}
	});

The above code hangs. I expected it to work the same as with res'. If I replace writablewithres` it works fine. Any ideas?

@fivethreeo
Copy link
Contributor Author

do you have any suspense or errorbouandries in the app?

@dglozic
Copy link

dglozic commented Jan 29, 2023

I do, yes (an ErrorBoundary React component). No Suspense yet.

@dglozic
Copy link

dglozic commented Jan 29, 2023

As I said, the App renders properly when I call pipe(res) in onShellReady(), but hangs when I swap res with writable.

@fivethreeo
Copy link
Contributor Author

Using express?

@dglozic
Copy link

dglozic commented Jan 29, 2023

Yes, and Node LTS 18

@fivethreeo
Copy link
Contributor Author

Try taking out the final handler

@dglozic
Copy link

dglozic commented Jan 30, 2023

I figured it out, but it took actually extending the Writable class:

	class MyWritable extends Writable {
		write(chunk, encoding, cb) {
			res.write(chunk, encoding, cb);
		}
	
		writev(chunks, encoding, cb) {
			res.writev(chunks, encoding, cb);
		}
	
		end(chunk, encoding, cb) {
			res.end(chunk, encoding, cb);
		}
	
		flush() {
			if (typeof res.flush === 'function') {
				res.flush();
			}
		}
	
		destroy(err) {
			res.destroy(err ?? undefined);
		}
	}

	const writable = new MyWritable();

@dglozic
Copy link

dglozic commented Jan 30, 2023

My speculation is that React calls other Writable methods that were not implemented in the simple constructor, but are when extending like this.

@fivethreeo
Copy link
Contributor Author

Ah, I will update the example in the pull.

@dglozic
Copy link

dglozic commented Jan 30, 2023

I don't know if the example itself is affected or not, I just noticed it myself in my own code. I am confused because I have seen other examples on the web similar to yours, yet I could not get it to work (I suspect something about closing the pipe).

@fivethreeo
Copy link
Contributor Author

Need some jest tests for testing with writeable too. I think a idea is forming on how to test this. Just need to separate all the components into their own app so I can test each kind of loadable component by itself. Then hook into the loadable events I add and do some mocking and rendering with renderToPipeableStream.

@stale
Copy link

stale bot commented Apr 2, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Apr 2, 2023
@stale stale bot closed this Jun 18, 2023
@thomaswelton
Copy link

Hey @fivethreeo thanks for your work so far on this.
Wanted to ask if the stale bot was overzealous, or if it is the case that this feature is now a wontfix and streaming support will not be coming. Thanks.

@theKashey
Copy link
Collaborator

I reckon we can "combine" this PR and #976 (in terms of getting rid of the "old" loadable) to get amazing results.
But as any job it requires somebody to work on it. Unfortunately nowadays this is not something I can afford.

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

Successfully merging this pull request may close these issues.

4 participants