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].tsx
Instead 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].tsx
Instead 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.tsx
In 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>}
getServerSideProps
Remix 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().push
Remix 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().replace
Remix 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().query
To 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().asPath
Next.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>}
getStaticProps
Remix 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.jsx
Remix 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.jsx
Remix 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
ErrorBoundary
used 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().events
Next.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> )}