Framework

Introduction

React Server Components (RSC) is an experimental approach to building server-rendered React applications. By running components directly on the server, developers can simplify data fetching, reduce client-side JavaScript, and maintain a more seamless, modular architecture. This guide explores how RSC can integrate with GraphQL queries and mutations to create modern, scalable web apps—without the complexity of legacy approaches like wrapping entire layouts in providers or managing extensive client-side state.

Getting Started

With React Server Components, you can call server actions directly from the UI while leveraging your existing GraphQL operations. This approach sidesteps the need for client-side wrappers like Apollo providers or libraries such as Axios within React components. Instead, RSC enables a more intuitive pattern: server components make direct GraphQL calls, and server actions expose these calls to the client for efficient data fetching and updates.

Consider the following example, where server actions trigger GraphQL mutations without requiring additional layers or complex setups:

export const closeReturnAction = async (returnId: string): Promise<string | undefined> => {
  try {
    await closeReturn(returnId);
  } catch (e) {
    return 'Error closing return';
  }
};

export const reopenReturnAction = async (returnId: string): Promise<string | undefined> => {
  try {
    await reopenReturn(returnId);
  } catch (e) {
    return 'Error reopening return';
  }
};

export const cancelReturnAction = async (returnId: string): Promise<string | undefined> => {
  try {
    await cancelReturn(returnId);
  } catch (e) {
    return 'Error closing return';
  }
};

By integrating these actions directly, we retain a clean, modular, and type-safe architecture. The result is a more maintainable codebase that can easily evolve as the application grows.

Data Fetching with statesetFetch

RSC excels at server-side data fetching. Below is an example of fetching a return object directly from the server, allowing your page component to receive fully-hydrated data without additional round trips:

export async function getReturn(returnId: string): Promise<Return | undefined> {
  const res = await statesetFetch<StatesetReturnOperation>({
    cache: 'no-store',
    query: getReturnQuery,
    variables: { returnId },
  });

  return res.body.data.returns_by_pk;
}

In this example, we use a typed operation (StatesetReturnOperation) to ensure end-to-end type safety. The server component (e.g. page.tsx) can directly call getReturn, removing the complexity of managing GraphQL queries in individual React components.

GraphQL Operations and Fragments

Keep your codebase organized and consistent by centralizing GraphQL queries, mutations, and fragments. This approach ensures you can quickly adapt your data schema without hunting through multiple files.

Query Example:

export const getReturnQuery = /* GraphQL */ `
  query getReturn($returnId: String!) {
    returns_by_pk(id: $returnId) {
      ...returnFields
    }
  }
  ${returnFragment}
`;

Fragment Example:

export const returnFragment = /* GraphQL */ `
  fragment returnFields on returns {
    id
    created_date
    amount
    action_needed
    condition
    customerEmail
    customer_id
    description
    enteredBy
    flat_rate_shipping
    order_date
    order_id
    reason_category
    reported_condition
    requested_date
    rma
    serial_number
    scanned_serial_number
    shipped_date
    status
    tax_refunded
    total_refunded
    tracking_number
    warehouse_received_date
    warehouse_condition_date
    refunded_date
  }
`;

Because all operations and fragments are stored in a centralized lib/stateset directory, you maintain a single source of truth, minimizing duplication and improving consistency.

Integrating Server Actions

Server actions call these GraphQL operations and return typed data. This approach provides a clearly defined interface for the UI to request data, enabling a smooth integration with server components and client-side interactivity:

export async function getReturns({
  limit,
  offset,
  order_direction
}: {
  limit?: number;
  offset?: number;
  order_direction?: string;
}): Promise<Return[]> {
  const res = await statesetFetch<StatesetReturnsOperation>({
    query: getReturnsQuery,
    variables: { limit, offset, order_direction },
    cache: 'no-store'
  });

  return res.body.data.returns;
}

From Server Component to Client

In your server-rendered pages, you can now retrieve data before sending it to the client. The component below demonstrates how data can be fetched with RSC and then hydrated in the browser for client-side interaction where necessary:

export default function ReturnPage({ returnId }: { returnId: string }) {
  const [returnData, setReturnData] = useState<Return | undefined>(undefined);

  useEffect(() => {
    const fetchData = async () => {
      const res = await getReturnAction(returnId);
      setReturnData(res);
    };
    fetchData();
  }, [returnId]);

  return (
    <>
      <TopBar />
      <div className="flex">
        <Sidebar />
        <div className="flex-1">
          <div className="px-4 mt-8 md:px-20 lg:px-32 space-y-4">
            <ReturnAnalytics returns_month={100} returns_today={5} />
          </div>
          <div className="mt-8 px-4 md:px-20 lg:px-32 space-y-4">
            <AdvancedReturnTable returns={returnData} offset={0} limit={20} />
          </div>
        </div>
      </div>
    </>
  );
}

By combining server components with server actions and a well-structured GraphQL strategy, you gain the best of both worlds—server-rendered efficiency and client-side responsiveness—without overly complicated data management layers.