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.
- 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
- 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
- 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
- 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
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β 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 β
ββββββββββββββββββββ
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
- 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
- Node.js 18+
- npm or yarn
# Clone and install dependencies
git clone <repository-url>
cd next-hydration-test
npm install# Start development server
npm run dev
# Visit http://localhost:3001npm 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// 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 },
],
}),
}),
});// 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>
);
};// 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 } };
}
);// 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: () => ({}),
});// 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',
});-
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
-
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
-
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
# 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- Visit
/lesson/language-models-intro - Note the progress percentage
- Click "Complete Exercise" button
- Progress updates immediately (optimistic update)
- Refresh page - data persists (server-side cache)
- Wait 5+ minutes and refresh - new random data (cache expired)
- Stop the development server
- Try interacting with components
- Observe error states and retry behavior
- Restart server - automatic recovery
// Global cache (rootApi.ts)
keepUnusedDataFor: 300, // 5 minutes
// Per-endpoint cache (Lessons.ts)
getLessonData: builder.query({
// ...
keepUnusedDataFor: 600, // 10 minutes for this endpoint
}),// Add to src/services/Lessons.ts
getQuizData: builder.query<QuizData, string>({
query: (quizId) => `/quiz-data?quizId=${quizId}`,
providesTags: ['QuizData'],
}),// Add error middleware
const rtkQueryErrorLogger: Middleware = (api) => (next) => (action) => {
if (action.type.endsWith('/rejected')) {
console.error('RTK Query Error:', action.error);
}
return next(action);
};- β 5-minute server cache reduces API calls
- β Automatic deduplication prevents duplicate requests
- β Background refetching keeps data fresh
- β Selective invalidation updates only changed data
- β Code splitting via endpoint injection
- β Tree shaking removes unused endpoints
- β Minimal runtime compared to traditional Redux
- β Auto-generated hooks for all endpoints
- β TypeScript integration with full type safety
- β DevTools support for debugging
- β Optimistic updates for better UX
- Fork the repository
- Create a feature branch
- Make your changes
- Test RTK Query integration thoroughly
- 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.