Skip to content
Dashboard

Migrating Grep from Create React App to Next.js

Link to headingWhy Next.js and React Server Components?

Link to headingPreserving an instant search experience

Link to headingRoot layout and repositioning

layout.tsx
export default function RootLayout({ children }) {
return (
<>
<script dangerouslySetInnerHTML={{ __html: "..repositioning logic.." }} />
<SearchBar />
<main>{children}</main>
</>
);
}

Link to headingConditional rendering and shared state

components/header.tsx
"use client";
export function Header() {
const pathname = usePathname();
return (
<header>
<Logo />
{pathname !== '/' && <SearchBar />}
<ThemeToggle />
</header>
);
}

layout.tsx
import { Header } from '@/components/header';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
</body>
</html>
);
}

page.tsx
export function Homepage() {
return (
<div className="search-centered">
<SearchBar />
</div>
);
}

Link to headingKeeping the search state in sync

Link to headingServer-first, client-second data fetching

Link to headingServer-side initial fetch and hydration

search/page.tsx
import { /* ... */} from "@tanstack/react-query";
import { ResultsClient } from "@/components/results";
import { apiSearch } from "@/lib/api";
import { getFiltersFromRawSearchParams } from "@/lib/utils";
const queryClient = new QueryClient({
defaultOptions: {
dehydrate: {
// Include pending queries so the client can pick them up
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending'
}
}
});
const searchOptions = (filters) => ({
queryKey: ["search", filters],
queryFn: () => apiSearch(filters),
});
export default function SearchPage({ searchParams }) {
const filters = getFiltersFromRawSearchParams(searchParams);
// Kick off the query on the server without 'await'
queryClient.prefetchQuery(searchOptions(filters));
return (
// Pass promise to client
<HydrationBoundary state={dehydrate(queryClient)}>
<ResultsClient />
</HydrationBoundary>
);
}

components/results.tsx
"use client";
import { useSuspenseQuery } from "@tanstack/react-query";
import { searchOptions } from "@/lib/queries";
export function ResultsClient() {
const filters = useFilters(); // client-side filter + input state
const { data } = useSuspenseQuery(searchOptions(filters));
return <Hits data={data} />;
}

Link to headingIncremental client-side fetching with TanStack Query

components/results.tsx
'use client';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useDeferredValue, Suspense } from 'react';
import { searchQueryOptions } from '@/lib/queries';
import { useNonOptimisticFilters } from '@/hooks/filters';
import { Hits } from '@/components/hits';
function ResultsInner({ filters }) {
const { data } = useSuspenseQuery(searchQueryOptions(filters));
return <Hits data={data} />;
}
export function ResultsClient() {
const filters = useNonOptimisticFilters();
const deferredFilters = useDeferredValue(filters);
return (
<Suspense fallback={<ResultsSkeleton />}>
<ResultsInner filters={deferredFilters} />
</Suspense>
);
}

Link to headingPreventing stale or out-of-order results

hooks/filters.ts
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useOptimistic, useTransition } from "react";
export function useFilters() {
const currentFilters = /* ... */;
const [optimisticFilters, setOptimisticFilters] = useOptimistic(
currentFilters,
(_, updatedFilters) => updatedFilters
);
const [isPending, startTransition] = useTransition();
const updateFilters = (updateFn) => {
const newFilters = updateFn(currentFilters);
setOptimisticFilters(newFilters);
// ...
startTransition(() => {
router.replace("/search?" + params.toString());
});
};
return { filters: optimisticFilters, updateFilters, isPending };
}

Link to headingPrefetching dynamic search routes

components/prefetch-search-layout.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export function PrefetchSearchLayout() {
const router = useRouter();
useEffect(() => {
router.prefetch("/search?q=");
}, [router]);
return null;
}

Link to headingSolving mobile-specific challenges

usePreventScroll.tsx
"use client";
import { useEffect } from "react";
export function usePreventScroll(isFocused: boolean) {
useEffect(() => {
document.body.style.overflow = isFocused ? "hidden" : "";
}, [isFocused]);
}

Link to headingAdditional performance gains with Partial Prerendering

Link to headingFinal results and what’s next for Grep

Ready to deploy?