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

RFC: React Server Components #188

Merged
merged 9 commits into from Oct 25, 2022
Merged

Conversation

josephsavona
Copy link
Contributor

@josephsavona josephsavona commented Dec 21, 2020

In this RFC, we propose introducing Server Components to React. We recommend watching our talk introducing Server Components before reading.

View the formatted RFC

@josephsavona josephsavona changed the title RFC: Server Components RFC: React Server Components Dec 21, 2020
@Daniel15
Copy link
Member

Daniel15 commented Dec 21, 2020

Will any part of the server-side be tightly coupled to Node.js, or could it run on a different JS runtime (for example, Rhino in Java, embedded V8 like with ClearScript for C#, ChakraCore, etc)? The only mention of Node in the RFC is around debugging ("you would debug your API in Node") but it's unclear as to whether server-side components would be tightly coupled to Node or not. Perhaps this is intentionally an open question at the moment. One of the major benefits and reasons for popularity of JavaScript is that it runs in lots of places, so it would be unfortunate to couple it to one particular implementation.

It does say this:

Server Components are designed from the beginning to be used with any meta-framework or into custom application setups

but that's still unclear as to whether "any meta-framework" implicitly means "any Node.js meta-framework"

I see a comparison to the older ASP.NET WebForms technology in the RFC, but it'd be interesting to compare it to ASP.NET Blazor too, which is a newer model where the components can either run server-side or client-side (via compilation to WebAssembly).

@plusplushq
Copy link

From what I read, meta-frameworks refers to other React Frameworks like Next.js and Gatsby

@behzad888
Copy link

behzad888 commented Dec 21, 2020

There are some things I really think will help. As I saw, the following questions came up.

Using SSR

If we use SSR, there will be two scenarios:

  • First, Using SSR and server components at the same time: This absolutely ignores the idea.
  • Second, Using SSR and server components as part of the application: I think this is a challenging idea. Although, this scenario parts our project into two sections, and it's not cheap.

Without SSR

We send our texts, images, and many pieces of information using JSON. How should we deal with SEO?

@josephsavona
Copy link
Contributor Author

josephsavona commented Dec 21, 2020

NOTE: edited to provide more context

Will any part of the server-side be tightly coupled to Node.js, or could it run on a different JS runtime

@Daniel15 React Server Components are conceptually decoupled from the JS runtime, but we also need some amount of environment-specific integration. For example, we currently support a Node.js integration for general consumption (used in the demo) and an internal integration for Relay. One of the main aspects of these integrations is handling the interaction between server/client components and packages (more details on that point in the related RFC). We can consider adding support for other environments (and are interested in the community's feedback about which we may want to prioritize).

From what I read, meta-frameworks refers to other React Frameworks like Next.js and Gatsby

@8brandon Yup, we're referring to frameworks such as Next.js and Gatsby. I'll update the text to clarify.

@markerikson
Copy link

@josephsavona other half of that question:

how feasible is it for any of this server functionality to work if your backend is not JS-based (ie, Python, Java, Go, etc)?

@themre
Copy link

themre commented Dec 21, 2020

@josephsavona other half of that question:

how feasible is it for any of this server functionality to work if your backend is not JS-based (ie, Python, Java, Go, etc)?

Yes, also very interested because this would allow to push for React on many areas where other server tech dominates and cannot be replaced.

text/0000-server-components.md Outdated Show resolved Hide resolved
@josephsavona
Copy link
Contributor Author

josephsavona commented Dec 21, 2020

how feasible is it for any of this server functionality to work if your backend is not JS-based (ie, Python, Java, Go, etc)?

This an interesting area for potential future exploration. As noted in the RFC, one of the benefits of React Server Components is that they allow developers to use a single language and framework to write an application, and share code across the server and client. We can foresee an ecosystem of packages forming around Server Components (libraries designed to be called from Server Components or Shared Components) which would not be available to developers attempting to use aspects of the Server Components architecture from other languages. Similarly, using a different language for the server would likely prohibit (or at least complicate) code sharing. Further, we expect to continue iterating on the response protocol for Server Components ("streaming JSON with slots") and are not yet ready to standardize it, which would be a prerequisite to making interop libraries for other languages. In summary, this is an interesting idea but it feels a bit premature.

@aspirisen
Copy link

Server Components are great idea, it will change the game!
One thing is that it would be pretty good to allow insert Server components into Client components as memo component. So every time when the props are changed we request server component to rerender and return the output.

import MyServerComponent from "./myServerComponent";

export function ClientComponent() {
  const [id, setId] = React.useState(0);
  return (
    <div>
      <button onClick={() => setId(Math.random())}>Select other id</button>
      <MyServerComponent id={id} />
    </div>
  );
}

So there will be pretty seamless integration and it will simple to extract heavy logic to server.

One question is how to import server component as you can't literally import this file.
Maybe something like this
const MyServerComponent = React.foreign(() => import('./myServerComponent'));
So there will be actual HTTP request to server, and server will be able to understand what component it needs and return an export object for react, also this component can be placed on any arbitrary URL. IDE along with TypeScript will understand what component is actually used in MyServerComponent variable. In addition you can delegate such kind of code to bundlers or transpilers so they can optimize it if you need, anyway in raw JavaScript that will also work.

@markerikson
Copy link

heh, translating:

"It's still experimental, we're still iterating, we're really focused on doing everything in JS for now for simplicity's sake. We won't rule out it maybe working with other languages, but we're not gonna worry about that for now".

Fair summary?

@dfabulich
Copy link

dfabulich commented Dec 21, 2020

The "why not use async/await" section of the FAQ IMO needs more detail. I think y'all have made the wrong call here.

Why don’t use just use async/await?
We’d still need a layer on top, for example, to deduplicate fetches between components within a single request. This is why there are wrappers around async APIs. You will be able to write your own. We also want to avoid delays in the case that data is synchronously available -- note that async/await uses Promises and incurs an extra tick in these cases.

A layer to deduplicate fetches sounds great, but that library could use async/await, too.

As it stands, libraries like react-fs and react-fetch appear to be synchronous, and they might be synchronous, depending on the status of the cache, but they might not be synchronous; there's no user-visible way to know. https://github.com/facebook/react/blob/master/packages/react-fs/src/ReactFilesystem.js

(To be clear, right now the code is using a, uh, very surprising pattern to make asynchronous code appear synchronous: if the result value is cached, it returns the value synchronously, but if not, the fetcher throws a Promise. You know, you'd normally throw exception objects, but JS lets you throw any value, so why not throw a Promise amirite?!? When React catches a Promise (a thenable) it awaits the result, caches it and then re-runs the React component; now the component won't throw a Promise and will run to completion normally.)

Using async/await will make this code substantially easier to understand, and the cost of a "tick" is trivial (and worth the price, especially in server components).

EDIT: Thinking about this a bit harder, I know the React team has been extremely resistant to async/await in components for years now. Fine. But that needs its own RFC. Some clear written document spelling out in detail why async/await is the wrong approach for React, and not just a comment on this RFC.

I'd like to ask the React team to write that RFC doc because I think it can't be written: you'll find that the argument falls apart when you try to explain it.

@josephsavona
Copy link
Contributor Author

@markerikson I appreciate that you're trying to get a clear takeaway. However, there are a lot of considerations here and I worry that trying to distill the complex tradeoffs I alluded to into a short summary may not be helpful to folks.

@ShanonJackson
Copy link

ShanonJackson commented Dec 21, 2020

My initial thoughts after watching the video are this:

All Components in peoples projects can either be thought of as 'Isomorphic' if that codebase is using server-side rendering, OR if that code base is not using serer side rendering then they're all 'client' components. For me personally I feel that introducing an API like this will now cause a abstraction layer where people using server side rendering will now need to think about 3 kinds of components instead of 1 (server/client/isomorphic) and the complexity of thinking about which 'context' you're in because in some contexts you can now only use JSON serializable properties.

For me and our team currently the biggest problem we have is scaling React applications which seems to start getting very hard around the (350+ routes) mark, I feel like this RFC although helps performance it hurts scalability as now we need to consider the implementations and find bugs in 3 types of components instead of 1.

Would be good to hear other peoples thoughts on this.

These are some observations on the API which are purely based on my opinion:

BundleSize / Server side code.

  • Removing a 83KB (20KB gzip) library isn't a big deal, I would say the bigger problem here is that you're using a 83KB library to format dates.
  • There's already methods to remove these libraries and allow for sever side code
    1: babel-plugin-preval allows you to use server-side code (at build time).
    2: NextJS has an implementation for getServerSideProps (server) and getInitialProps (isomorphic) that use the current implementation of React to achieve server side code, and potentially tree shaking in a very simple abstraction.
// tree shakes out date-fns
ComponentName.getServerSideProps= async (ctx) => {
      const { format } = require("date-fns");
     return {
           formatted: format(new Date(), "M/d/yy")
     }
}

I will concede that the API you're proposing is more powerful. However the point being made here is that there are already methods out there currently that exist to achieve the majority of this API's functionality in peoples projects.

Data Fetching

  • Already possible with relay as mentioned in the video
  • Already possible probably through build tools if your routes are structured in a way that can be statically analyzed you can probably use a combination of pre-rendering, examining the data dependencies and "lifting" that into the parent during build time. (No implementation's seem to exist yet, but I feel like NextJS is starting to cause alot of innovation in this space).
  • Already possible in an infinite number of other ways as JavaScript is a very dynamic language I've posted a simple implementation below.
export const Person(data) {
     return <span>{data.name}</span>
}
Person.data = () => api("/api/person")

// and if that Person has a dependency to fetch its data.
export const Person(data) {
     return <span>{data.name}</span>
}
Person.data = (id) => api("/api/person", {body: {id: 1}});

Just traverse your React sub-tree from the parent and run all the data methods. Your API is definitely cleaner and I wouldn't recommend to anyone to use the above code, its just there to show that its possible and its definitely possible in other ways as well.

XSS

  • Just going to leave this here as a note for implementers but obviously the second you allow JSX to be sent over the network you're going to allow for user input to be say.... a script tag with dangerouslySetInnerHTML or a iframe with data:uri. Just needs to be considered/mitigated.

One of the reason I loved hooks is that it solved REAL problems for us that were currently preventing scale in our React applications, where-as I feel like for me personally this doesn't solve real problems I'm currently facing in my React projects.

For me personally, I wouldn't trade a fractional increase in performance for the complexity this brings to a project, especially when I have many other levers I can pull before pulling this one.

@hamedmam
Copy link

hamedmam commented Dec 21, 2020

I really liked the bundle size reduction for the code that is rendered on the server, specially when it comes to third-party packages that are not doing a great job in code-splitting and tree-shaking,
however the data fetching has limited use cases IMO.
The idea of decoupling backend and client for large scale applications (microservice architecture) will really stop people from thinking to fetch data from components on the server and in the same language (node), I still strongly believe it has it's own use cases for static marketing websites or smaller scale apps.
That being said, I am still impressed with the work, thank you for making web development easier and more accessible everyday.

@yazaddaruvala
Copy link

I feel this RFC should discuss the nuances of how deployments will need to be handled. Deployments require two versions of the server side code, and two versions of the client side code to all play nicely together.

It's relatively common to ensure an API is backwards compatible for one or two deployments before cleaning up the old code. However, what does it mean for a Server Component to be backwards compatible?

For example, the heuristics with an API: "adding new fields" is just fine, "removing fields" needs extra care.

What are the heuristics for a Server Component? Currently it seems like a size (height or width), scroll-type, or even theme change will be backwards incompatible.

@benjamingr
Copy link

benjamingr commented Dec 21, 2020

I read the RFC (thank you for working on making this sort of use case easier and more efficient!).

I am very confused by this sort of code:

import db from 'db.server';

function Note({id}) {
  const note = db.notes.get(id);
  return <NoteWithMarkdown note={note} />;
}

All the Node APIs are async - so in the above db.notes.get(id) returns a promise (or blocks Node.js for every other user using it). How are users supposed to work around this in server components since according to the RFC there are no effects or async/await?

I saw in the video Lauren mentions a fetch API that is "as if it's synchronous so we don't have to wrap it in an effect" - how does that work?

@josephsavona
Copy link
Contributor Author

XSS

@ShanonJackson Thanks for raising this point. We considered security from the start when designing this proposal and the streaming protocol guards against injection attacks. I'll add a note to reflect this.

@benjamingr
Copy link

Actually I installed the experimental version of react-fetch and I see it's just suspense - it might be beneficial to release an adapter for "generic" Node APIs that throw when the promise is pending.

Interesting.

(As a random nit - you're using Resolved to mean Fulfilled in the code)

@Kukkimonsuta
Copy link

This is interesting concept, however I'm not sure for how large portion of the react community it actually solves any problems. If I understand correctly I can't use this (or I'm limited in using this) when I'm

  • not running javascript on server
  • using mutable state (ex. mobx)
  • using non-serializable state (ex. symbols, classes)
  • depend on reference equality

Considering these points I'm very unlikely to ever use it, so let me ask - how pay to play is this? How aware do existing react packages need to be of this existing? Does it affect performance/bundle size even when I'm not using it?

@benjamingr
Copy link

using mutable state (ex. mobx)

Why would that not work?


As a side note, I think it's worth exposing (in a react-server-components package or something) the utilities used throughout things like react-pg and react-fetch like createRecordFromThenable and readRecordValue.

@Kukkimonsuta
Copy link

Kukkimonsuta commented Dec 21, 2020

@benjamingr I may be wrong, but I don't think the server side component can be made observable, so it wouldn't update when property of an object passed in through prop changes. I also believe observability is stripped from object when going through serialization (getters/setter/proxy information is not serializable) so this could break any client components called by the server component as well.

@perilevy
Copy link

@theKashey You are talking about whether it's testable, of course it's testable.
But it doesn't mean it's pure. useEffect is by definition a hook for side effects.
I suggest you to read again my first comment, I'm not talking about tests at all.
I also suggest you to read the reference I attached about side effects to understand why it's better to have a pure logic
(hint: not just for tests).

A quote:

it’s important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state. Unfortunately, it can be difficult to detect these problems as they can often be non-deterministic.

@theKashey
Copy link

You did not try abstract as I've asked. And I am not sure where you found "tests" in my message.
Note that API used in Server Components examples, as well as for Suspense render-as-you-fetch, is not "purely" fetch. It's some abstraction, with something like cache layer, and other strange things where you can ".fetch" data in one place and .read in another.

Well, instead of thousands words - https://github.com/reactjs/server-components-demo/blob/2d9fb948b7073f5f07e22d71350422ee9e1cc7f3/src/Note.server.js#L9

@perilevy
Copy link

@theKashey Well fetch-mock is a library for tests, probably a misunderstanding.
The cache layer doesn't make it pure, you can also read from fs, db, etc. as described in the talk. These all IO actions are side effects.
Even if we use a pattern, of injecting the data to cache in another place, it makes the server component a controlled component which waits for its provider data, and that provider component won't be pure.

@gaearon
Copy link
Member

gaearon commented May 10, 2021

@perilevy

We learned that it's not recommended that function bodies would contain side effects, because it's part of the render phase which should be deterministic (pure function, see ref).

We're going to need to make this a bit more nuanced. Technically React components need not be strictly pure in the mathematical definition, but they need to be idempotent. This is what makes I/O possible, but only with very a limited contract which doesn't break idempotency. So React I/O libraries like react-fetch, react-pg, etc, will be allowed, because they will follow that strict contract (here is a temporary description before it gets into the docs: facebook/react#17526 (comment)).

This still doesn't mean that you can do arbitrary I/O like actually running fetch or fs etc from your components. They must go through React wrappers that work with Suspense and follow that contract.

The idempotency requirement applies to both Server and Client components. There are no differences there. React I/O and Suspense would work exactly the same way on the client.

@leaveLi
Copy link

leaveLi commented May 17, 2021

If there is such an implementation,it will be very interesting:

function DemoServer ({id, children}) {
  const res = fetch(`http://a.com/content/${id}`).json()
  return children && children(res)
}

<DemoServer id={1}>
  {(res) => <DemoClient data={res} />}
</DemoServer>

something like "component as a service"

CAAS, is fun😂

@darren-at-shell
Copy link

i still don't believe the exploit of serialisation has been addressed. If we are allowing for serialisable react components to be sent back from the server, then we cannot use the current security mechanism of providing "Symbols" to separate our user generated json "fake react structures" from real ones.

Ie the very old bug mentioned here - https://medium.com/dailyjs/exploiting-script-injection-flaws-in-reactjs-883fb1fe36c1

What is stopping someone crafting user input of the above style (albeit maybe slightly different now) like the following:

{
 _isReactElement: true,
 _store: {},
 type: "body",
 props: {
   dangerouslySetInnerHTML: {
     __html:
     "<h1>Arbitrary HTML</h1>
     <script>alert(‘No CSP Support :(')</script>
     <a href='http://danlec.com'>link</a>"
    }
  }
}

@devknoll
Copy link

devknoll commented Jun 23, 2021

i still don't believe the exploit of serialisation has been addressed. If we are allowing for serialisable react components to be sent back from the server, then we cannot use the current security mechanism of providing "Symbols" to separate our user generated json "fake react structures" from real ones.

Ie the very old bug mentioned here - https://medium.com/dailyjs/exploiting-script-injection-flaws-in-reactjs-883fb1fe36c1

What is stopping someone crafting user input of the above style (albeit maybe slightly different now) like the following:

{
 _isReactElement: true,
 _store: {},
 type: "body",
 props: {
   dangerouslySetInnerHTML: {
     __html:
     "<h1>Arbitrary HTML</h1>
     <script>alert(‘No CSP Support :(')</script>
     <a href='http://danlec.com'>link</a>"
    }
  }
}

Can you elaborate what your concern is? React won’t render plain JSON objects (in modern browsers) — an element needs a $$typeof whose value is a symbol. The only way to generate that symbol is to run code on the client, and the only way the deserializer will do that is when it’s expecting to parse an element.

The only way (in the current implementation) it expects to parse an element is to read an array where element 0 is exactly the string “$”. The serializer won’t serialize a user string without escaping it, and any string that is exactly “$” is escaped as “$$” until it’s subsequently parsed and unescaped (as a string, not an element or symbol). And it will only serialize an exact string “$” when it is serializing an element — by checking that it has the symbol defined (which can only be set by executing code on the server).


One new consideration in the design of Suspense from this proposal is that we would like to use a consistent API for accessing data across Server Components, Client Components, and Shared Components. Overall, though, the design of Suspense is outside the scope of this RFC. We agree that we should document this design clearly and will prioritize doing so in the new year.

### Are Server Components refetched whenever their props change?
Copy link

Choose a reason for hiding this comment

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

The content below is that even if the parent Client Component is refetched, the child Server Component is not re-rendered.
So I think that the title should be like this,
Are Server Components refetched whenever their parent Client Components re-rendered?.
I understood that Server Components should refetched when their props changed.

@brillout
Copy link

brillout commented Jul 27, 2021

I'm wondering how crawlers handle HTML streams? Do they wait until the HTML stream ends? In that case we could have SEO optimized pages with a fast TTFB. That would be a big deal. (I've created StackOverflow - SEO/crawlability impact of HTML Streaming.)

@wmertens
Copy link

Caching results:

Would it be possible for each SC to indicate that it's cacheable with some key (via a hook) so that the result can be stored and immediately served when the same key is encountered?

The cache retrieval could happen before rendering. Based on the request, some cache keys are generated and if you find a fully matching set, you serve the page immediately.

@jansivans
Copy link

jansivans commented Aug 15, 2021

I have released initial implementation and docs for new server-side component framework Drayman - http://www.drayman.io/
It is not a substitute for React because it works differently and has other use-cases, but it also uses JSX syntax and some concepts are similar to React Server Components (like working directly with file system, databases, etc.), so I am here to help and provide info on how it was built and maybe something will become handy for this RFC.

@brillout
Copy link

Is there a plan regarding errors happening mid-stream? Imagine a page that is already 80% rendered but an error occurs in a React component in the last 20% of the HTML stream.

Seems like there doesn't seem to be a way to cancel/overwrite already sent HTML (but I didn't dig too much; I may have overlooked a solution).

I guess the only way is to have React overwrite the DOM to show the error page. But I wonder how the mechanics would work here. Seems like there need to be some (new?) API between React and the React framework (vite-plugin-ssr, Next.js, ...). There is React Error Boundaries but I wonder how that would work with Server Components.

Is it something the React team has already thought about?

I'm currrently implementing HTML streaming support for vite-plugin-ssr (shameless plug :-)), and I'm super looking forward to add support for React Server Components.

Some prior discussion in the Marko community: marko-js/community#1.

@mikeposh
Copy link

Perhaps using isToday from 'date-fns' (in the demo) is not a good example of something than can run on the server to save sending date formatting code to the client?

The result of isToday for a fixed time will depend on the client's timezone, which won't be known on the server.

@gaearon
Copy link
Member

gaearon commented Mar 24, 2022

Yes, it's not a perfect example.

@lmatteis
Copy link

How do you re-render a server-component (say you want to fetch new data) if it cannot be imported by a client-component (who would setState and change the server-component props)?

@gaearon
Copy link
Member

gaearon commented Oct 25, 2022

Thanks everyone for the comments! We've made a number of changes in response to your feedback:

I've updated this RFC to link to those other RFCs with details. We're going to go forward with this as the first iteration.

@gaearon gaearon merged commit c8ad461 into reactjs:main Oct 25, 2022
1 check passed
@penx
Copy link

penx commented Nov 7, 2022

I've added SSR to the original demo:

https://github.com/penx/server-components-ssr

As we don't want the react-server codemod applied to the ReactDOMServer code, I'm running a separate worker thread with the codemod in it. I guess in production we could build out 2 applications (one with the codemod and one without) making this unnecessary, but it's fairly handy for development purposes!

One thing I haven't figured out yet is how to deal with webpack_chunk_load and webpack_require on the server:

https://github.com/facebook/react/blob/8e2bde6f2751aa6335f3cef488c05c3ea08e074a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js#L75

https://github.com/facebook/react/blob/8e2bde6f2751aa6335f3cef488c05c3ea08e074a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js#L94

For these to work in ReactDOMServer I guess they need to be polyfilled?

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