Remixed Relay: Evolution of React Server Component
Tags: react, reactservercomponents, relay
Published: 2024-07-12 03:14:02

React Server Components (RSC) represents a significant advancement in how server-rendered content is handled in React applications. The concept of React Server Components draws inspiration from various previous technologies and patterns, particularly Relay. Relay, a JavaScript framework for building data-driven React applications, provided foundational concepts that have influenced the development of React Server Components. This article delves into how React Server Components evolved from Relay, highlighting the similarities, differences, and advancements.

Understanding Relay
What is Relay?
Relay is a JavaScript framework developed by Facebook (now Meta) for managing and fetching GraphQL data in React applications. It was designed to handle complex data requirements and provide a highly optimized data-fetching layer for React.

Key Features of Relay
GraphQL Integration: Relay works seamlessly with GraphQL, allowing for precise data fetching and efficient updates.
Declarative Data Fetching: Components declare their data dependencies using GraphQL fragments, making data requirements explicit and co-located with the components.
Optimistic Updates: Relay supports optimistic UI updates, allowing the UI to reflect changes before the server confirms them.
Efficient Data Fetching: Relay minimizes over-fetching and under-fetching by composing multiple data requirements into a single query.

The Concept of Server Components
What are React Server Components?
React Server Components (RSC) allow developers to build components that run on the server and send HTML to the client. This approach aims to optimize server-side rendering (SSR) by splitting the rendering workload between the server and the client.

Key Features of React Server Components
Server-Side Execution: Components can be executed on the server, reducing the client-side JavaScript bundle size.
Direct Data Access: Server Components can directly access server-side data sources, such as databases or APIs, without additional client-side data fetching.

Seamless Integration: RSC can be seamlessly integrated with client components, enabling a hybrid rendering model.
Improved Performance: By offloading rendering to the server, React Server Components can improve the initial load performance and SEO.
Evolution from Relay to React Server Components

Similarities:

Declarative Data Requirements: Both Relay and React Server Components emphasize the declarative nature of data requirements. In Relay, components declare their GraphQL fragments, while in RSC, server components can directly fetch data and render HTML.

Optimized Data Fetching: Relay’s efficient data fetching mechanism influenced RSC’s ability to directly access and fetch data on the server, reducing the need for multiple client-side requests.
Component Co-location: In both Relay and RSC, data fetching logic is co-located with the components, making the data dependencies explicit and easier to manage.

Differences:
Rendering Paradigm: Relay focuses on optimizing client-side data fetching and updates using GraphQL, while React Server Components shift part of the rendering workload to the server, sending pre-rendered HTML to the client.

Server-Side Execution: Relay operates entirely on the client side, fetching data and updating the UI. RSC executes components on the server, leveraging server resources to generate HTML and send it to the client.

Data Fetching: Relay relies on GraphQL for data fetching, requiring a GraphQL server and schema. RSC can fetch data from any server-side data source, including REST APIs, databases, or other services, without being tied to GraphQL.

Advancements in React Server Components
Simplified Data Access: RSC simplifies data access by allowing server-side code to fetch data directly, avoiding the need for additional client-side data-fetching logic.

Reduced Client-Side Overhead: By moving part of the rendering logic to the server, RSC reduces the amount of JavaScript that needs to be executed on the client, leading to improved performance and faster initial loads.

Hybrid Rendering: RSC supports a hybrid rendering model where server components can be combined with client components, providing flexibility in rendering strategies.

Improved SEO: Server-side rendering with RSC improves SEO by delivering pre-rendered HTML content to search engines, making it easier for them to crawl and index the content.

Example: Transition from Relay to React Server Components
Relay Example
import { graphql, QueryRenderer } from 'react-relay';
import environment from './environment';

const App = () => (
  <QueryRenderer
    environment={environment}
    query={graphql`
      query AppQuery {
        user(id: "1") {
          name
        }
      }
    `}
    render={({ error, props }) => {
      if (error) {
        return <div>Error!</div>;
      }
      if (!props) {
        return <div>Loading...</div>;
      }
      return <div>User: {props.user.name}</div>;
    }}
  />
);

export default App;

How Relay’s Innovations Were Brought to React Through React Server Components

Relay introduced several groundbreaking features and methodologies for data handling and fetching in React applications. These features provided developers with a robust framework for managing complex data dependencies and optimizing client-side rendering. 

React Server Components (RSC) have taken some of these innovations and integrated them into a server-rendered paradigm, enhancing the capabilities of React applications. This section emphasizes how the core functionalities of Relay have been adapted and evolved into React Server Components.

1) Declarative Data Fetching: From Relay to React Server Components
Relay’s Approach
Relay: In Relay, data fetching is declarative. Each component specifies its data requirements using GraphQL fragments, which are then composed into a single query by Relay. This approach ensures that data dependencies are clear and co-located with the component that uses them.

import { graphql, QueryRenderer } from 'react-relay';

const UserComponent = () => (
  <QueryRenderer
    environment={environment}
    query={graphql`
      query UserComponentQuery {
        user(id: "1") {
          name
          email
        }
      }
    `}
    render={({ error, props }) => {
      if (error) {
        return <div>Error!</div>;
      }
      if (!props) {
        return <div>Loading...</div>;
      }
      return (
        <div>
          <h1>{props.user.name}</h1>
          <p>{props.user.email}</p>
        </div>
      );
    }}
  />
);

export default UserComponent;

How was it solidified in RSC: React Server Components also use a declarative approach to data fetching, but with a key difference: data fetching occurs on the server. This allows for direct and efficient access to server-side resources without additional client-side requests.

// UserComponent.server.js
import React from 'react';

const fetchUserData = async (id) => {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
};

const UserComponent = async ({ id }) => {
  const user = await fetchUserData(id);
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

export default UserComponent;

2) Optimized Data Fetching and Minimization
Relay’s Approach
Relay: Relay optimizes data fetching by composing multiple component queries into a single network request. This minimizes the number of requests and ensures that only the required data is fetched.

import { commitMutation, graphql } from 'react-relay';

function updateUser(environment, userId, newName) {
  const mutation = graphql`
    mutation UpdateUserMutation($input: UpdateUserInput!) {
      updateUser(input: $input) {
        user {
          id
          name
        }
      }
    }
  `;

  const variables = {
    input: {
      id: userId,
      name: newName,
    },
  };

  commitMutation(environment, {
    mutation,
    variables,
    optimisticResponse: {
      updateUser: {
        user: {
          id: userId,
          name: newName,
        },
      },
    },
    onCompleted: (response, errors) => {
      console.log('Mutation completed');
    },
    onError: (err) => console.error(err),
  });
}

How was it solidified inRSC: React Server Components leverage the server’s ability to directly fetch and render data, eliminating the need for multiple client-side data fetching operations. This approach inherently minimizes network requests and optimizes the data fetching process.

3) Component Co-location and Data Dependency Management
Relay’s Approach
Relay: Data requirements are co-located with the components that use them, making it easier to manage and understand data dependencies.

const UserFragment = graphql`
  fragment UserComponent_user on User {
    name
    email
  }
`;

const UserComponent = ({ user }) => (
  <div>
    <h1>{user.name}</h1>
    <p>{user.email}</p>
  </div>
);

export default createFragmentContainer(UserComponent, {
  user: UserFragment,
});
How was it solidified in RSC: In React Server Components, data fetching and rendering logic are similarly co-located. This maintains clarity and modularity, allowing developers to understand and manage data dependencies within the component itself.

// UserComponent.server.js
const fetchUserData = async (id) => {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
};

const UserComponent = async ({ id }) => {
  const user = await fetchUserData(id);
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

export default UserComponent;

4) Improved Performance and SEO:
Relay’s Approach
Relay: Relay improves client-side performance by reducing over-fetching and providing mechanisms for efficient updates. However, because it operates on the client side, initial loading times and SEO can still be challenging.

How was it solidified in RSC: React Server Components significantly enhance performance and SEO by rendering components on the server. The server can send fully-rendered HTML to the client, reducing the amount of JavaScript needed on the client side and providing immediate content for search engines to crawl.

// Server-side rendering logic
import { renderToString } from 'react-dom/server';
import App from './App';

const serverRender = (req, res) => {
  const html = renderToString(<App />);
  res.send(`<!DOCTYPE html>
<html>
<head>
  <title>React Server Components</title>
</head>
<body>
  <div id="root">${html}</div>
  <script src="/bundle.js"></script>
</body>
</html>`);
};

5) Hybrid Rendering Model
Relay’s Approach
Relay: While Relay focuses on client-side data management and rendering, it does not inherently support server-side rendering.

How was it solidified in RSC: React Server Components introduce a hybrid rendering model, where server-rendered components can seamlessly integrate with client-rendered components. This hybrid approach allows developers to leverage the benefits of both server-side and client-side rendering within the same application.

// App.server.js
import React from 'react';
import HeaderComponent from './HeaderComponent.server';
import FooterComponent from './FooterComponent';
import UserComponent from './UserComponent.server';

const App = () => (
  <div>
    <HeaderComponent />
    <UserComponent id="1" />
    <FooterComponent />
  </div>
);

export default App;
6) Enhanced Developer Experience
Relay’s Approach
Relay: Relay provides tools and conventions for managing GraphQL data, but it requires developers to understand and work with GraphQL schemas, queries, and mutations.

How was it solidified in RSC: React Server Components simplify the developer experience by allowing direct access to server-side data sources without requiring GraphQL. This reduces the learning curve and allows developers to use familiar REST APIs or other data sources.

7) Optimistic Updates:
Relay’s Approach
Relay: Relay supports optimistic updates, allowing the UI to be updated immediately based on an expected result while the actual mutation request is processed in the background. This feature improves the user experience by providing instant feedback.

import { commitMutation, graphql } from 'react-relay';

function updateUser(environment, userId, newName) {
  const mutation = graphql`
    mutation UpdateUserMutation($input: UpdateUserInput!) {
      updateUser(input: $input) {
        user {
          id
          name
        }
      }
    }
  `;

  const variables = {
    input: {
      id: userId,
      name: newName,
    },
  };

  commitMutation(environment, {
    mutation,
    variables,
    optimisticResponse: {
      updateUser: {
        user: {
          id: userId,
          name: newName,
        },
      },
    },
    onCompleted: (response, errors) => {
      console.log('Mutation completed');
    },
    onError: (err) => console.error(err),
  });
}
How was it solidified in RSC: React Server Components (RSC) provide a robust framework for handling optimistic updates. By leveraging server-side rendering, RSC can pre-render components with expected data changes, ensuring immediate UI feedback while maintaining consistency and integrity.

import { use, Suspense } from 'react-server-dom';

function UserComponent({ userId, optimisticData }) {
  const user = use(fetchUser(userId));
  return (
    <div>
      <h1>{optimisticData ? optimisticData.name : user.name}</h1>
      {optimisticData ? null : <p>Loading...</p>}
    </div>
  );
}

async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

async function updateUser(userId, newName) {
  // Simulate a delay for updating user
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return { id: userId, name: newName };
}

// Example usage with optimistic UI
function App({ userId }) {
  const optimisticData = { name: 'Optimistic Name' }; // Mock optimistic data

  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserComponent userId={userId} optimisticData={optimisticData} />
    </Suspense>
  );
}

8) Subsequent Data Fetching
Relay optimizes subsequent data fetching through:

Declarative Data Requirements: Components declare their data needs, and Relay fetches the necessary data.
Query Batching: Relay batches multiple GraphQL queries into a single request, reducing network overhead.
Caching: Relay caches query results, minimizing redundant network requests.
Automatic Refetching: Relay refetches data when variables change, keeping the UI up-to-date.
// Client-Side Data Fetching with Relay
import { useQueryLoader, usePreloadedQuery } from 'react-relay/hooks';
import { MyQuery } from './MyQuery';

function MyComponent() {
  const [queryReference, loadQuery] = useQueryLoader(MyQuery);

  useEffect(() => {
    loadQuery({ id: 'some-id' });
  }, [loadQuery]);

  if (!queryReference) {
    return <div>Loading...</div>;
  }

  return <DisplayData queryReference={queryReference} />;
}

function DisplayData({ queryReference }) {
  const data = usePreloadedQuery(MyQuery, queryReference);
  return <div>{data.user.name}</div>;
}
How was it solidified in RSC: React Server Components (RSC) take the concept of subsequent data fetching further by performing data fetching on the server. This approach offloads the responsibility of data fetching from the client to the server, enhancing performance and reducing client-side complexity.

import { use } from 'react-server-dom';

function UserComponent({ userId }) {
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

export default UserComponent;

Conclusion:
Relay introduced several advanced features that have profoundly influenced the development of data-driven React applications. These features, including declarative data fetching, optimized data fetching and minimization, component co-location, and data dependency management, have set a high standard for managing complex data needs in React.

React Server Components have taken these foundational concepts and adapted them to enhance server-side rendering capabilities. By leveraging server-side execution, React Server Components offer significant improvements in performance, SEO, and developer experience. The transition from Relay’s client-side optimizations to RSC’s server-side capabilities represents a natural evolution, providing developers with powerful tools to build more efficient and performant applications.

As React continues to evolve, the integration of these advanced features into the core framework through React Server Components demonstrates React’s commitment to addressing the challenges of modern web development and providing inn