Skip to content

Task 4: RSC Version (10-14 hours) #7

@AbanoubGhadban

Description

@AbanoubGhadban

Overview

Build the React Server Components (RSC) + streaming version.

Dependency: Task 2 (Shared Components & API)
Can Run Parallel With: Task 3 (Traditional Version)

Key Architecture

  • Root component is async function WITHOUT "use client"
  • Components go into the RSC bundle
  • Entire page is SSRed - both server components AND any client components
  • Data fetched server-side via getReactOnRailsAsyncProp
  • HTML streamed to browser as Suspense boundaries resolve

Deliverables

Rails View

<!-- app/views/restaurants/search_rsc.html.erb -->
<div class="search-page">
  <%= stream_react_component("SearchPageRSC", { restaurant_id: @restaurant.id }) %>
</div>

Components

Component Type Purpose
SearchPageRSC.tsx Server (async) Entry point, no hooks
AsyncStatus.tsx Server (async) Awaits status server-side
AsyncWaitTime.tsx Server (async) Awaits wait time server-side
AsyncSpecials.tsx Server (async) Awaits specials server-side
AsyncTrending.tsx Server (async) Awaits trending server-side
AsyncRating.tsx Server (async) Awaits rating server-side

Pattern

// SearchPageRSC.tsx - Async server component
async function SearchPageRSC({ restaurant_id }: Props) {
  return (
    <div>
      <RestaurantCardHeader restaurantId={restaurant_id} />
      <Suspense fallback={<Spinner />}>
        <AsyncStatus restaurantId={restaurant_id} />
      </Suspense>
    </div>
  );
}
export default SearchPageRSC;

// AsyncStatus.tsx - Server component (async, no hooks)
async function AsyncStatus({ restaurantId }: Props) {
  const status = await getReactOnRailsAsyncProp('status', { restaurantId });
  return <StatusBadge status={status} />;
}
export default AsyncStatus;

Rails Async Prop Helpers

# app/helpers/async_props.rb
module AsyncProps
  def self.status(restaurant_id:)
    restaurant = Restaurant.find(restaurant_id)
    { status: restaurant.current_status, timestamp: Time.current.to_i }
  end
  # ... similar for wait_time, specials, trending, rating
end

Performance Monitoring

  • Collect Web Vitals (same as Traditional)
  • Measure "Stream Complete Time", "Server Fetch Time"

Success Criteria

  • /search/rsc page loads and renders
  • All data fetched server-side (before rendering)
  • HTML streamed to browser with Suspense boundaries
  • Spinners resolved on server (no client-side spinners visible)
  • API calls ONLY on server (no fetch() in browser Network tab)
  • LCP: ~200-250ms
  • CLS: ~0.02 (no layout shifts)
  • 59% faster than Traditional (550ms → 225ms)
  • Console shows no errors

Server Component Constraints

Server components CANNOT:

  • Use useState, useEffect, useCallback, etc.
  • Use useContext
  • Use browser APIs (localStorage, window, etc.)
  • Be marked with "use client"

Server components CAN:

  • Be declared as async function
  • Await data fetching
  • Access Rails helpers and models
  • Render pure display components
  • Include client components lower in the tree (with "use client")

Key Files to Create

app/javascript/entries/search_rsc.tsx
app/javascript/components/async/rsc/
├─ SearchPageRSC.tsx
├─ AsyncStatus.tsx
├─ AsyncWaitTime.tsx
├─ AsyncSpecials.tsx
├─ AsyncTrending.tsx
└─ AsyncRating.tsx
app/views/restaurants/search_rsc.html.erb
app/helpers/async_props.rb

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions