Skip to content

Proof of concept: Next.js app with precompiled MDX + dynamic components

Notifications You must be signed in to change notification settings

Apoorve73/next-hydration-test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

14 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Next.js Hydration Test - RTK Query Integration

This project demonstrates a hybrid architecture combining static precompiled MDX content with dynamic React components that fetch data using RTK Query, all integrated with server-side caching and hydration. This architecture mirrors patterns used in SkillUp frontend and serves as a proof of concept for efficient data fetching and caching strategies.

πŸš€ Key Features

1. RTK Query Integration

  • RTK Query for efficient data fetching and caching
  • 5-minute server-side caching (configurable)
  • Automatic cache invalidation on mutations
  • Server-side prefetching with cache hydration

2. Hybrid Content Architecture

  • Static Content: MDX files compiled at build-time using next-mdx-remote
  • Dynamic Components: React components that fetch real-time data via RTK Query
  • Seamless Integration: Both content types coexist on the same page

3. Advanced Caching Strategy

  • Server-side cache: 5 minutes (300 seconds)
  • Automatic retries: Up to 2 retries on failure
  • Cache invalidation: Smart invalidation on data updates
  • Optimistic updates: Immediate UI feedback

4. Interactive MDX Components

  • Embedded React Components: Custom components directly in markdown
  • Interactive Elements: Code blocks, quizzes, and progress trackers
  • Real-time Updates: Components update cache and trigger re-renders

πŸ—οΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Static MDX    β”‚    β”‚  RTK Query       β”‚    β”‚  Next.js API    β”‚
β”‚   Content       │◄──►│  Components      │◄──►│  Routes         β”‚
β”‚                 β”‚    β”‚                  β”‚    β”‚                 β”‚
β”‚ β€’ Lesson text   β”‚    β”‚ β€’ useQuery hooks β”‚    β”‚ β€’ GET /lesson   β”‚
β”‚ β€’ Code examples β”‚    β”‚ β€’ useMutation    β”‚    β”‚ β€’ POST /lesson  β”‚
β”‚ β€’ Quizzes       β”‚    β”‚ β€’ Cache mgmt     β”‚    β”‚ β€’ Data storage  β”‚
β”‚ β€’ Embedded      β”‚    β”‚ β€’ Invalidation   β”‚    β”‚ β€’ Business logicβ”‚
β”‚   components    β”‚    β”‚                  β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                       β”‚                       β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   RTK Query      β”‚
                    β”‚   Cache Store    β”‚
                    β”‚                  β”‚
                    β”‚ β€’ 5min cache     β”‚
                    β”‚ β€’ Server hydrate β”‚
                    β”‚ β€’ Auto refetch   β”‚
                    β”‚ β€’ Optimistic UI  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“ Project Structure

src/
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ InteractiveCodeBlock.tsx    # Embeddable code component
β”‚   β”œβ”€β”€ InteractiveQuizButton.tsx   # RTK Query mutation button
β”‚   β”œβ”€β”€ ProgressTracker.tsx         # RTK Query data component
β”‚   └── QuizWidget.tsx              # Interactive quiz component
β”œβ”€β”€ lib/
β”‚   └── mdx.ts                      # MDX compilation utilities
β”œβ”€β”€ pages/
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   └── lesson-data.ts          # API route (GET/POST)
β”‚   β”œβ”€β”€ lesson/
β”‚   β”‚   └── [slug].page.tsx         # Dynamic lesson pages
β”‚   β”œβ”€β”€ _app.page.tsx               # App wrapper with RTK Query
β”‚   └── index.page.tsx              # Homepage
β”œβ”€β”€ services/
β”‚   β”œβ”€β”€ common/
β”‚   β”‚   β”œβ”€β”€ baseQuery.ts            # Base query configuration
β”‚   β”‚   └── rootApi.ts              # Root RTK Query API
β”‚   └── Lessons.ts                  # Lesson-specific endpoints
└── content/
    └── language-models-intro.mdx   # Sample lesson content

πŸ”§ Technology Stack

  • Next.js 15.2.4 - React framework with Page Router API
  • React 18.3.1 - UI library
  • RTK Query (via @reduxjs/toolkit 2.9.0) - Data fetching and caching
  • next-redux-wrapper 8.1.0 - Redux SSR integration
  • next-mdx-remote 5.0.0 - MDX compilation
  • gray-matter 4.0.3 - Frontmatter parsing
  • TypeScript 5.9.2 - Type safety

πŸš€ Getting Started

Prerequisites

  • Node.js 18+
  • npm or yarn

Installation

# Clone and install dependencies
git clone <repository-url>
cd next-hydration-test
npm install

Development

# Start development server
npm run dev

# Visit http://localhost:3001

Available Scripts

npm run dev        # Start development server on port 3001
npm run build      # Build for production
npm run start      # Start production server
npm run type-check # Run TypeScript checks

πŸ“– RTK Query Usage Examples

1. Service Definition

// src/services/Lessons.ts
export const lessonsAPI = rootApi.injectEndpoints({
  endpoints: (builder) => ({
    getLessonData: builder.query<LessonData, string>({
      query: (lessonId) => `/lesson-data?lessonId=${lessonId}`,
      providesTags: (result, error, lessonId) => [
        { type: 'LessonData', id: lessonId },
      ],
      keepUnusedDataFor: 300, // 5 minutes
    }),
    
    updateLessonProgress: builder.mutation<LessonData, UpdateArgs>({
      query: ({ lessonId, exerciseCompleted }) => ({
        url: `/lesson-data`,
        method: 'POST',
        body: { lessonId, action: 'complete_exercise' },
      }),
      invalidatesTags: (result, error, { lessonId }) => [
        { type: 'LessonData', id: lessonId },
      ],
    }),
  }),
});

2. Component Usage

// Using RTK Query hooks
const ProgressTracker: React.FC = () => {
  const { data, isLoading, error } = useGetLessonDataQuery('lesson-id');
  const [updateProgress, { isLoading: isUpdating }] = useUpdateLessonProgressMutation();

  const handleUpdate = async () => {
    try {
      await updateProgress({ lessonId: 'lesson-id', exerciseCompleted: true }).unwrap();
    } catch (error) {
      console.error('Update failed:', error);
    }
  };

  return (
    <div>
      {isLoading ? 'Loading...' : `Progress: ${data?.progress}%`}
      <button onClick={handleUpdate} disabled={isUpdating}>
        {isUpdating ? 'Updating...' : 'Complete Exercise'}
      </button>
    </div>
  );
};

3. Server-side Prefetching

// pages/lesson/[slug].page.tsx
export const getServerSideProps: GetServerSideProps = wrapper.getServerSideProps(
  (store) => async ({ params }) => {
    const slug = params?.slug as string;
    
    // Prefetch data server-side
    await store.dispatch(lessonsAPI.endpoints.getLessonData.initiate(slug));
    
    // Wait for all queries to complete
    await Promise.all(store.dispatch(lessonsAPI.util.getRunningQueriesThunk()));
    
    return { props: { lesson } };
  }
);

🎯 RTK Query Configuration

Root API Setup

// src/services/common/rootApi.ts
export const rootApi = createApi({
  reducerPath: 'rootApi',
  baseQuery: retry(baseQuery, { maxRetries: 2 }),
  keepUnusedDataFor: 300, // 5 minutes global cache
  extractRehydrationInfo(action, { reducerPath }) {
    if (action.type === HYDRATE) {
      return action.payload[reducerPath];
    }
    return undefined;
  },
  tagTypes: ['LessonData', 'LessonContent'],
  endpoints: () => ({}),
});

Store Configuration

// src/store/index.ts
export const makeStore = (ctx?: Context) =>
  configureStore({
    reducer: {
      [rootApi.reducerPath]: rootApi.reducer,
    },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        thunk: { extraArgument: ctx },
        serializableCheck: {
          ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
        },
      }).concat(rootApi.middleware),
    devTools: process.env.NODE_ENV !== 'production',
  });

πŸ”„ Data Flow with RTK Query

  1. Server-Side (SSR):

    • Compile MDX content from filesystem
    • Prefetch data using RTK Query endpoints
    • Populate RTK Query cache server-side
    • Render complete page with cached data
  2. Client-Side (Hydration):

    • React hydrates the server-rendered HTML
    • RTK Query cache rehydrates with server data
    • Components use cached data immediately
    • Background refetching as needed
  3. Runtime Updates:

    • User interactions trigger RTK Query mutations
    • Mutations automatically invalidate related cache entries
    • Components re-render with fresh data
    • Optimistic updates provide immediate feedback

πŸ§ͺ Testing the RTK Query Integration

API Endpoints Test

# Test GET endpoint
curl http://localhost:3001/api/lesson-data?lessonId=language-models-intro

# Test POST endpoint  
curl -X POST -H "Content-Type: application/json" \
  -d '{"lessonId":"language-models-intro","action":"complete_exercise"}' \
  http://localhost:3001/api/lesson-data

Caching Test

  1. Visit /lesson/language-models-intro
  2. Note the progress percentage
  3. Click "Complete Exercise" button
  4. Progress updates immediately (optimistic update)
  5. Refresh page - data persists (server-side cache)
  6. Wait 5+ minutes and refresh - new random data (cache expired)

Error Handling Test

  1. Stop the development server
  2. Try interacting with components
  3. Observe error states and retry behavior
  4. Restart server - automatic recovery

🎨 Customization

Adjusting Cache Duration

// Global cache (rootApi.ts)
keepUnusedDataFor: 300, // 5 minutes

// Per-endpoint cache (Lessons.ts)
getLessonData: builder.query({
  // ...
  keepUnusedDataFor: 600, // 10 minutes for this endpoint
}),

Adding New Endpoints

// Add to src/services/Lessons.ts
getQuizData: builder.query<QuizData, string>({
  query: (quizId) => `/quiz-data?quizId=${quizId}`,
  providesTags: ['QuizData'],
}),

Custom Error Handling

// Add error middleware
const rtkQueryErrorLogger: Middleware = (api) => (next) => (action) => {
  if (action.type.endsWith('/rejected')) {
    console.error('RTK Query Error:', action.error);
  }
  return next(action);
};

πŸš€ Performance Benefits

Caching Efficiency

  • βœ… 5-minute server cache reduces API calls
  • βœ… Automatic deduplication prevents duplicate requests
  • βœ… Background refetching keeps data fresh
  • βœ… Selective invalidation updates only changed data

Bundle Optimization

  • βœ… Code splitting via endpoint injection
  • βœ… Tree shaking removes unused endpoints
  • βœ… Minimal runtime compared to traditional Redux

Developer Experience

  • βœ… Auto-generated hooks for all endpoints
  • βœ… TypeScript integration with full type safety
  • βœ… DevTools support for debugging
  • βœ… Optimistic updates for better UX

πŸ“š Learning Resources

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Test RTK Query integration thoroughly
  5. Submit a pull request

Built with ❀️ as a proof of concept for RTK Query integration in modern Next.js applications with 5-minute server-side caching.

About

Proof of concept: Next.js app with precompiled MDX + dynamic components

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published