Skip to content
Dashboard

Migrating Grep from Create React App to Next.js

Copy link to headingWhy Next.js and React Server Components?

Copy link to headingPreserving an instant search experience

Copy link to headingRoot layout and repositioning

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

Copy 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>
);
}

Copy link to headingKeeping the search state in sync

Copy link to headingServer-first, client-second data fetching

Copy 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} />;
}

Copy 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>
);
}

Copy 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 };
}

Copy 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;
}

Copy 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]);
}

Copy link to headingAdditional performance gains with Partial Prerendering

Copy link to headingFinal results and what’s next for Grep

Ready to deploy?