Do you like this guide? If yes you may want to reserve a spot for the Remix for Next Devs Video Course where we will create a full Remix application from scratch using Remix, Tailwind, Supabase, Docker and Fly.
Table of Contents
Instead of using folders and slashes to define routes, you can use dots (.) to define routes, each dot define a path segment.
pages/├── _app.tsx├── index.tsx├── about.tsx├── concerts/│ ├── index.tsx│ ├── trending.tsx│ └── [city].tsx
app/├── routes/│ ├── _index.tsx│ ├── about.tsx│ ├── concerts._index.tsx│ ├── concerts.$city.tsx│ ├── concerts.trending.tsx│ └── concerts.tsx└── root.tsx
[city].tsxInstead of using square brackets to define dynamic routes, you can use the dollar sign with your param name ($city) to define dynamic routes.
pages/├── _app.tsx├── concerts/│ ├── index.tsx│ └── [city].tsx
app/├── routes/│ ├── concerts._index.tsx│ ├── concerts.$city.tsx└── root.tsx
[...slug].tsxInstead of using three dots to define catch all routes, you can use the dollar sign ($) to define catch all routes.
pages/├── _app.tsx├── posts/│ ├── [...slug].tsx│ └── index.tsx
app/├── routes/│ ├── posts.$.tsx│ └── posts._index.tsx└── root.tsx
Route groups exist in Next.js app directory, Remix has them too, if a route starts with a underscore it will be used as an hidden route, useful to define a layout for a set of routes.
app/├── (group)/│ ├── folder/│ │ ├── page.tsx│ │ └── layout.tsx│ ├── page.tsx│ └── layout.tsx├── other/│ └── page.tsx├── layout.tsx
app/├── routes/│ ├── _group.tsx│ ├── _group._index.tsx│ ├── _group.folder.tsx│ └── other.tsx└── root.tsx
You can escape dots in Remix with [] syntax. This is useful for characters like . and _ that have special meaning in the route syntax.
pages/├── _app.tsx├── posts/│ ├── index.tsx│ └── about.tsx├── sitemap.xml.tsx
app/├── routes/│ ├── posts._index.tsx│ ├── posts.about.tsx│ └── sitemap[.xml].tsx└── root.tsx
_document.tsxIn Remix, the equivalent of _document.tsx in Next.js is root.tsx.
// /pages/_document.tsximport { Html, Head, Main, NextScript } from 'next/document'export default function Document() { return ( <Html lang='en'> <Head /> <body> <Main /> <NextScript /> </body> </Html> )}
// app/root.tsximport { Links, Meta, Outlet, Scripts, ScrollRestoration,} from '@remix-run/react'export default function Root() { return ( <html lang='en'> <head> <Links /> <Meta /> </head> <body> <Outlet /> <ScrollRestoration /> <Scripts /> </body> </html> )}
In Remix, you can define layouts in the app directory, the equivalent of _app.tsx in Next.js is root.tsx. Each route folder can have a layout too, simply define a component for that folder and use Outlet to render the child routes.
// app/posts/layout.tsxexport default function Layout({ children }) { return <div>{children}</div>}// app/posts/[id]/page.tsxexport default function Page() { return <div>Hello World</div>}
import { Outlet } from '@remix-run/react'// app/routes/posts.tsxexport default function Layout() { return ( <div> <Outlet /> </div> )}// app/routes/posts.$id.tsxexport default function Page() { return <div>Hello World</div>}
getServerSidePropsRemix has loader instead of getServerSideProps, the loader function is a top-level export in a route module that is used to fetch data for the route. This function is called on every render, on client side navigation this function will be used to get the json for the next page.
// /pages/index.tsxexport async function getServerSideProps() { const data = await fetchData() return { props: { data } }}const Page = ({ data }) => <div>{data}</div>export default Page
// /routes/index.tsximport { LoaderFunction, json } from '@remix-run/node'import { useLoaderData } from '@remix-run/react'export let loader: LoaderFunction = async (request) => { const data = await fetchData() return json(data)}export default function Index() { let data = useLoaderData<typeof loader>() return <div>{data}</div>}
getServerSideProps with redirectRemix has an utility function called redirect you can return in your loaders, notice that this function simply returns a Response.
export async function getServerSideProps() { return { redirect: { destination: '/home', permanent: false, }, }}
import { LoaderFunction, redirect } from '@remix-run/node'export let loader: LoaderFunction = async () => { return redirect('/home', { status: 307 })}
getServerSideProps notFoundRemix supports throwing responses, similar to what Next.js app directory does, when you throw a response you can intercept it in a route ErrorBoundary to show a custom message.
export async function getServerSideProps() { return { notFound: true, }}
import { LoaderFunction } from '@remix-run/node'export let loader: LoaderFunction = async () => { throw new Response('', { status: 404 })}
Remix has no concept of API routes, just use normal loaders like any other route and return a Response object.
// /pages/api/hello.tsimport { NextApiRequest, NextApiResponse } from 'next'export default async function handler( req: NextApiRequest, res: NextApiResponse,) { res.status(200).json({ name: 'John Doe' })}
// /routes/api/hello.tsimport { LoaderFunctionArgs, LoaderFunction } from '@remix-run/node'export let loader = async ({ request }: LoaderFunctionArgs) => { const res = new Response(JSON.stringify({ name: 'John Doe' })) return res}
useRouter().pushRemix instead of useRouter has many little hooks unfortunately. One of these is useNavigate which is used to navigate to a new route.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return ( <button onClick={() => { router.push('/home') }} > Home </button> )}
import { useNavigate } from '@remix-run/react'export default function Index() { const navigate = useNavigate() return ( <button onClick={() => { navigate('/home') }} > Home </button> )}
useRouter().replaceRemix uses navigate with a second options argument.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return ( <button onClick={() => { router.replace('/home') }} > Home </button> )}
import { useNavigate } from '@remix-run/react'export default function Index() { const navigate = useNavigate() return ( <button onClick={() => { navigate('/home', { replace: true }) }} > Home </button> )}
useRouter().reload()In Next.js you can reload with router.reload() or router.replace(router.asPath). In Remix you can use revalidate from useRevalidator.
In Remix revalidate loading state is not the same as
useNavigation.state, this means if you want to create a progress bar at the top of the page you will also need to use this revalidator state too to show the loading bar during reloads or form submits.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return ( <button onClick={() => { router.reload() }} > Reload </button> )}
import { useRevalidator } from '@remix-run/react'export default function Index() { const { revalidate } = useRevalidator() return ( <button onClick={() => { revalidate() }} > Reload </button> )}
useRouter().queryTo access query parameters in Remix, you can use the useSearchParams hook.
Remix will not pass params in this object, unlike Next.js.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return ( <button onClick={() => { router.replace({ query: { ...router.query, name: 'John Doe' } }) }} > {router.query.name} </button> )}
import { useSearchParams } from '@remix-run/react'export default function Index() { const [searchParams, setSearchParams] = useSearchParams() return ( <button onClick={() => setSearchParams((prev) => { prev.set('name', 'John Doe') return prev }) } > {searchParams.get('name')} </button> )}
useRouter().asPathNext.js has asPath to get the current path as shown in the browser. Remix has useLocation, which returns an object similar to the window.location object.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return <div>{router.asPath}</div>}
import { useLocation } from '@remix-run/react'export default function Index() { const location = useLocation() return <div>{location.pathname}</div>}
useRouter().back()Remix uses the navigate function to go back in the history stack.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return ( <button onClick={() => { router.back() }} > Back </button> )}
import { useNavigate } from '@remix-run/react'export default function Index() { const navigate = useNavigate() return ( <button onClick={() => { navigate(-1) }} > Back </button> )}
useRouter().forward()Remix uses the navigate function to go forward in the history stack.
import { useRouter } from 'next/router'export default function Index() { const router = useRouter() return ( <button onClick={() => { router.forward() }} > Forward </button> )}
import { useNavigate } from '@remix-run/react'export default function Index() { const navigate = useNavigate() return ( <button onClick={() => { navigate(1) }} > Forward </button> )}
Dynamic params in Remix can be accessed both in the loaders and with an hook useParams.
import { useRouter } from 'next/router'export function getServerSideProps({ params }) { return { props: { params } }}export default function Index({ params }) { const router = useRouter() return ( <div> {params.name} is same as {router.query.name} </div> )}
import { LoaderFunctionArgs, json } from '@remix-run/node'import { useParams } from '@remix-run/react'export function loader({ params }: LoaderFunctionArgs) { return json({ params })}export default function Index() { const params = useParams() return <div>{params.name}</div>}
getStaticPropsRemix does not have a direct equivalent to getStaticProps, but you can use loader with a stale-while-revalidate cache control header to achieve the same behavior. You will also need a CDN on top of your host to support this feature the same way Next.js on Vercel does.
One drawback is that you can't create the pages ahead of time to have them fast on the first load.
export function getStaticProps({ params }) { return { props: { params } }}export const revalidate = 60export default function Index({ params }) { return <div>{params.name}</div>}
import { LoaderFunctionArgs, json } from '@remix-run/node'import { useLoaderData } from '@remix-run/react'export function loader({ params }: LoaderFunctionArgs) { return json( { params }, { headers: { // you will need a CDN on top 'Cache-Control': 'public, stale-while-revalidate=60', }, }, )}export default function Index() { const data = useLoaderData<typeof loader>() return <div>{data.params.name}</div>}
_error.jsxRemix can have an error boundary for each route, this error boundary will be rendered when you throw an error in a loader or during rendering
function Error({ statusCode }) { return ( <p> {statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'} </p> )}Error.getInitialProps = ({ res, err }) => { const statusCode = res ? res.statusCode : err ? err.statusCode : 404 return { statusCode }}export default Error
import { useRouteError, Scripts, isRouteErrorResponse } from '@remix-run/react'// root.tsxexport function ErrorBoundary() { const error = useRouteError() return ( <html> <head> <title>Oops!</title> </head> <body> <h1> {isRouteErrorResponse(error) ? `${error.status} ${error.statusText}` : error instanceof Error ? error.message : 'Unknown Error'} </h1> <Scripts /> </body> </html> )}
400.jsxRemix does not have a special file for 400 errors, you can use the error boundary to show a custom message for 400 errors.
Notice that the same Remix
ErrorBoundaryused for runtime errors is also called for 404 errors, you can check if the error is a response error to show a not found message.
// pages/400.jsxexport default function Custom404() { return <h1>404 - Page Not Found</h1>}
// root.tsximport { useRouteError, Scripts, isRouteErrorResponse } from '@remix-run/react'// a 404 page is the same thing as an error page, where the error is a 404 responseexport function ErrorBoundary() { const error = useRouteError() return ( <html> <head> <title>Oops!</title> </head> <body> <h1> {isRouteErrorResponse(error) ? `${error.status} ${error.statusText}` : error instanceof Error ? error.message : 'Unknown Error'} </h1> <Scripts /> </body> </html> )}
useRouter().eventsNext.js pages directory has router events, perfect to show progress bar at the top of the screen. Remix can do the same thing with the useNavigation hook.
import { useRouter } from 'next/router'import { useEffect, useState } from 'react'export default function Index() { const router = useRouter() const [isNavigating, setIsNavigating] = useState(false) useEffect(() => { router.events.on('routeChangeStart', () => setIsNavigating(true)) router.events.on('routeChangeComplete', () => setIsNavigating(false)) router.events.on('routeChangeError', () => setIsNavigating(false)) }, [router.events]) return <div>{isNavigating ? 'Navigating...' : 'Not navigating'}</div>}
import { useNavigation } from '@remix-run/react'export default function Index() { const { state } = useNavigation() return <div>{state === 'loading' ? 'Navigating...' : 'Not navigating'}</div>}
Next.js support streaming when using the app directory and server components, when you fetch a page you get the suspense fallback first while the browser streams the rest of the page and React injects script tags at the end to replace the fallbacks with the real components.
Remix can do the same, using the defer utility function. You pass unresolved promises and Remix can start render the page and replace the fallbacks with the rendered components later on time.
// app/page.tsx using server componentsimport { Suspense } from 'react'async function ServerComponent() { const data = await fetchData() return <div>{data}</div>}export default function Page() { return ( <Suspense fallback={<div>Loading...</div>}> <ServerComponent /> </Suspense> )}
import { defer } from '@remix-run/node'import { useLoaderData, Await } from '@remix-run/react'import { Suspense } from 'react'export function loader() { return defer({ data: fetchData(), })}export default function Page() { const { data } = useLoaderData<typeof loader>() return ( <Suspense fallback={<div>Loading...</div>}> <Await resolve={data}>{(data) => <div>{data}</div>}</Await> </Suspense> )}
Remix supports the React.lazy function to load components dynamically.
import dynamic from 'next/dynamic'const Page = dynamic(() => import('./page'), { loading: () => <div>Loading...</div>,})export default function App() { return <Page />}
import { lazy, Suspense } from 'react'const Page = lazy(() => import('./page'))export default function App() { return ( <Suspense fallback={<div>Loading...</div>}> <Page /> </Suspense> )}