sholladay / pogo

Server framework for Deno
Mozilla Public License 2.0
482 stars 33 forks source link

Allow React support to be optional #33

Closed luvies closed 4 years ago

luvies commented 4 years ago

It would be nice if React support was optional, such that if you didn't want to use it, the dependencies aren't downloaded. Currently, the entire React library is imported whether or not you want to use it, which might not be ideal for something like a pure API.

It could be done by allowing React to be added in manually by importing something like react.ts (which would be next to main.ts) and using its exports, or by allowing React to be imported by the consumer and then provided to the API (this would allow users to select a specific version of React as well, or maybe even be able to use Preact).

I'm not sure on the best way to implement it, but if this is something worth looking into, I'd be happy to try and write up a PR!

sholladay commented 4 years ago

I'm conflicted on this. On the one hand, I get your point that not everyone will need it, and being able to supply your own React/Preact version would be a genuinely nice feature. On the other hand, it does complicate things, and I think most people do need some sort of template engine / view library.

Part of what's great about Deno is that it can compile JSX out of the box, and I leverage this in Pogo to make it trivial to build webpages with React components. You don't have to do anything complicated with them, you can just use plain HTML syntax if you want to.

To be clear, the important bit that Pogo itself uses is ReactDOMServer and its renderToStaticMarkup() function, which is relatively small and simple. Unfortunately, we do download React too, but only to use its isValidElement() function (here). If Deno supported tree shaking, then that wouldn't be a big deal at all because barely anything would get loaded. And if Deno had an equivalent to Node's peerDependencies, then we could use that to leave the version up to the user. Sadly, Deno doesn't have either of those at the moment, so it's not as optimized as I would like.

What we could do would be to make something like server.register(), where you plug in your own React library. But that adds an extra step to do any sort of view templating. Is that worth it? Maybe.

Ultimately, I think it would be nice to build something like Next.js for Deno, probably as a layer built on top of Pogo. When something like that is available, it would give me more incentive to remove React from Pogo's core.

The good news is that, because of how Pogo is designed, you can probably use Preact already. I haven't tried it, but as long as Preact elements pass React.isValidElement() and can be serialized by ReactDOMServer.renderToStaticMarkup(), which I believe they should, then it will absolutely work. And the version of React whose React.createElement() function is called is totally in your hands as the user, as the elements are created in user code, outside of Pogo.

luvies commented 4 years ago

Would it be possible to convert the serialize into middleware? So after the handler has returned the value, you then iterate through the 'post-request' middlewares and each can modify the response object (i.e. serialise it). That way, you can add a custom middleware that renders JSX only if you import it.

I don't believe that pogo has proper middleware support, so personally I was thinking something like this:

interface Middleware {
  preRequest(req: Request): Request | undefined;
  postRequest(res: Response): Response | undefined;
}

Having it like this allows postRequest to return the changed object (as this follows the API style for the rest of the module). It could support constructing middleware for each individual request so that instance properties work as intended, which would allow things like timing middlewares for measuring request times.

sholladay commented 4 years ago

Yep, I've been thinking about how I want to approach middleware / extension points a lot recently and updating serialize() is likely part of that.

Pogo is modeled after hapi, which uses a well-structured request lifecycle with specific extension points that are pretty similar to what you are proposing. (See the docs and this diagram)

Being able to render things in a onPreResponse would be useful and I think that's a reasonable way to approach supporting React externally, if necessary.

luvies commented 4 years ago

It might be worth opening another issue for middleware I think. I agree, I think if serialize was made into an onPreResponse handler it would allow a React renderer to sit in front on the same extension point and render JSX before anything else without it needing to be bound inside the main serialisation.

The only possible issue I can think of is that the React renderer would have to always have to be called before the main serialisation, otherwise it wouldn't work at all, but that should be alright as long as handlers maintain an order.

I could look into creating a PoC PR for based on hapi's extension points, but it might be worth discussing about whether it's worth modifying it or using a different system to better suit pogo first.

sholladay commented 4 years ago

PR welcome for server.register() and/or server.ext(), just keep in mind that I haven't decided yet whether I want to actually remove React. I really like the simplicity of the current design.

What precisely made you think about this originally, if I may ask? Are you working on an API server? Did you run into problems with the current React version? Just want to use a different view library?

luvies commented 4 years ago

I'll have a look at writing a PR then 😊. I've had a look at how hapi implements ext and the internals are very complicated, so I think I'll try to go for something a little more simple for the implementation (it would have the same API, but we could look at iterating on it if needed).

I thought about this because I wanted to look into writing my own MVC-like framework (similar to Nest.js I think, but I like simple and explicit so not identical) and use pogo as the underlying server. Making React optional then came from the fact that nearly all the servers I have written/will write are pure APIs with SPA fronts, meaning React really shouldn't be included if it doesn't need to be.

I do agree that having React built-in is nice (it actually makes me want to try writing a MPA using pogo or my framework when I build it 😅), so my plan was to provide a React ext/plugin that can be just added in without issue (i.e. react_[ext|plugin].ts next to main.ts that imports React and exports a handler function or plugin).

luvies commented 4 years ago

I'm going to open an issue for middleware since it seems a bit out of scope for this issue (and I wanted to suggest a possible API for it)

sholladay commented 4 years ago

I've had a look at how hapi implements ext and the internals are very complicated, so I think I'll try to go for something a little more simple for the implementation

Yeah, most of hapi's internals are relatively simple and beautiful (e.g. the Response class or the bounce module), but the event system and extension points are a little wonky by comparison. The fact that server.ext() and server.on() are different APIs is slightly weird to me, for example. It's probably because extension points like onPreResponse can return a value whereas handlers for other types of events are not supposed to. But I don't really like it, conceptually they are all just events. I imagine we can simplify this in Pogo by implementing server.on() first and then - if it's even necessary - maybe server.ext() can just be an alias or shortcut akin to how server.router.get() is just a shortcut. We also don't need to implement hapi's advanced event handling features, I've never needed them.

I thought about this because I wanted to look into writing my own MVC-like framework (similar to Nest.js I think, but I like simple and explicit so not identical) and use pogo as the underlying server. Making React optional then came from the fact that nearly all the servers I have written/will write are pure APIs with SPA fronts, meaning React really shouldn't be included if it doesn't need to be.

That's great! I haven't tried Nest.js, but I've used Next.js and the thing I like about it is that it encourages server-side rendering, which is important for the health of the web. I feel that SPAs have caused a lot of accidental harm in terms of usability and reliability, SEO, etc. People will do whatever is convenient, so I want to make it easy for people to fix those problems with server-side rendering. To do it like Next.js does, you'd probably want to use a combo of ReactDOMServer.renderToStaticMarkup() for the page and ReactDOMServer.renderToString() for the app container element (relevant code in Next.js is here). If you render it yourself that way, it will bypass Pogo's usage of React, but Pogo will still send it as HTML.

luvies commented 4 years ago

I've opened an issue for middleware (#34) with some suggestions. I agree, it would be ideal to have 1 good API for handling this kind of thing. How useful would making server.ext() a shortcut be? It might be good to just keep it simple for now (I'm personally not a great fan of the name ext honestly 😅) and make it flexible.

I'll definitely have a look at Next.js, it would be nice to support SSR, but it wouldn't be top priority until I have something that works well I think 😅. SSR is definitely something I'll look at building into the framework, but if it gets that complicated I would prefer to split it out into either framework or pogo middleware (the latter would be nicer I think).

sholladay commented 4 years ago

The reason for aliasing server.ext() would be to make it easier to port hapi applications to Pogo. I realize that's kind of a niche case. At the moment, I've managed to maintain API compatibility for everything I've added without any real complexity. It's not crucial to keep doing that, but I do have a lot of Node.js/hapi apps I would like to port to Deno/Pogo over time and I think it would help encourage people to adopt Deno.

luvies commented 4 years ago

I see that, and Hapi's API is good (if a little large, but if I used Hapi I imagine I'd get used to it), so it's worth trying to keep compatibility where possible. Although, events in Hapi uses Node-like events, which don't really follow Deno very well. I'd probably recommend events be done either via AsyncIterables or something like Evt, as both allow good typing.

sholladay commented 4 years ago

Having thought about this for a few days, here's what I've decided:

  1. React support will remain built-in. It's a selling point and I personally use it a lot. Deno renders JSX out-of-the-box, and most backend projects will need a view engine of some sort, so we might as well leverage it.
  2. The user ought to have some control over the React version used by the framework, somewhat akin to Node's peerDependencies. For that, I will accept a PR that adds a react server option, which can be used to override the built-in version that ships with the framework. However, I would encourage users to use an import map instead, so that only one React is ever downloaded. PR welcome for documenting how to do that.

I'm happy to revisit this approach in the future. In particular, if Deno's tooling around peer dependencies improves, that may open up new opportunities for us.