Migrating a Next.js web application to Remix

  • Open a terminal window in the parent directory of your future web application
  • Initialize the Remix application with
    npx create-remix
  • Use the up and down arrow keys of the keyboard to select between the options.
    • The answer to the “Where would you like to create your app?” question will create the directory for the new application (./MY_NEW_APP)
    • To the “What type of app do you want to create?” question
      • “A pre-configured stack ready for production” sets up a production ready fully configured application based on the answer to “Which Stack do you want?”
        See https://remix.run/stacks for information on the stacks.
      • To set up a Remix application without additional frameworks, select “Just the basics”
    • Select the “Express Server” deployment target for high performance Docker container (EKS) deployments.
    • It is highly recommended to use TypeScript for production-grade applications.
  • CD into the new application and read the README.md file for deployment instructions.

Application configuration

At this point Remix version 1 is “forward compatible”. It already contains the version 2 features, and you can start to use them before the official release. To enable the version 2 features in version 1

  • Make sure the module.exports = { section of the “remix.config.js” file contains the following lines:
  future: {
    v2_errorBoundary: true,
    v2_headers: true,
    v2_meta: true,
    v2_normalizeFormMethod: true,
    v2_routeConvention: true,

Migration notes

Referencing files

Instead of @ use ~ to refer to the root of the site, and export the reference as a function, beca


import styles from '@/styles/footer.css'


import styles from '~/styles/footer.css'


Use the dot notation to specify the page hierarchy. See Route nesting in Remix version 2

Linking to other pages

Place your .tsx files into the /app/routes directory. Use dot notation to mark the routing level.

For simple links use the HTML <a /> command to link to pages. Start the URL with / to refer to the root of the site.


<a target="_blank"
  Remix Docs


Place the images into the /public directory and refer to them with path relative to the public directory

<img src="my-logo.png"/>

For more functionality use <Image /> from react-image at https://github.com/Josh-McFarlin/remix-image to resize, convert, adjust and cache images.

      size: { width: 100, height: 100 },
      maxWidth: 500,
      size: { width: 600, height: 600 },
  dprVariants={[1, 3]}

CSS Style Sheets

Place style sheet files into the /app/styles directory. Create is if it does not exist.

Shared (global) styles

Create the global style sheet

For global CSS code create the /app/styles/global.css file and place commonly used style instructions there.

Reference the style sheet in the app/root.tsx file

Add the following code to the app/root.tsx file. Make sure the App() function contains the <Links /> component in the <head> section to generate the <link rel=”stylesheet” href=”… /> HTML instructions in every page header.

// Import LinksFunction to make the <Links/> component work
import type { LinksFunction } from "@remix-run/node";

// Import the global style sheet available for all pages
import styles from "~/styles/global.css";

// Expose the style sheet to the application
export const links: LinksFunction = () => {
  return [
      rel: "stylesheet",
      href: styles,

// The <Links /> component will create the <link ... HTML instruction in the <head> of all pages to load the style sheet
export default function App() {
  return (
    <html lang="en">
        <Links />

Page specific styles

IMPORTANT: The <Link /> component has to be in the <head> section of the App() function in the app/root.tsx file to generate the <link rel=”stylesheet” href=”… /> HTML instructions in every page header. See Reference the style sheet in the app/root.tsx file above.

Place the code shown below into the route. Style sheet loading does not work from the component. In the example below, even if the <Header /> or <Footer /> component use the style, we need to place the import and const statements into the app/routes/about.tsx file. Use the <div className=”my-css-class”> syntax to reference the class in the CSS file.

import Header  from './_header';
import Footer  from './_footer';

// ========================================
// Import the page specific style sheet

// Import the style sheet
import styles from "~/styles/_footer.css";

// Import the LinksFunction
import type { LinksFunction } from "@remix-run/node";

// Expose the imported stylesheet to the <Links /> module
export const links: LinksFunction = () => {
  return [
      rel: "stylesheet",
      href: styles,

// ========================================

// The browser script
export default function About() {
  return (
      {/* Display the Header component */}

     <div className="my-css-class">About the site</div>

      {/* Display the Footer component */}

Route styles

If the style sheet is imported in the page, Remix uses route specific style sheet loading.

If you load  a style sheet into app/routes/dashboard.tsx with

import styles from "~/styles/dashboard.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];

and into app/routes/dashboard.accounts.tsx with

import styles from "~/styles/accounts.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];

The app/routes/dashboard.accounts.tsx page will actually load both

  • ~/styles/dashboard.css and
  • ~/styles/accounts.css

For more information see Styling in Remix

Server Side Rendering (SSR)

If a feature does not render correctly on the server, like react-select, because of its use of emotions, use <ClientOnly /> from  remix-utils at https://github.com/sergiodxa/remix-utils. Create a placeholder for the ClientOnly component, in this example a 32 wide <div />, to be able to predict the location on the final page. 

import Select from "react-select"
import { ClientOnly } from "remix-utils"

const CustomDropdown = () => {
  return (
    <div className="w-32">
        <Select />

Application logic


Remix simplifies the data transfer between the server side and browser side script, but we need to place the browser side code into the route. loader() and action() functions do not work in components, so we cannot use React tabs to switch between pages. All pages have to have their own URLs. 

In Next.js the entire script, saved in the pages directory, ran in the browser. The entire script saved in the pages/api directory ran on the server. We had to make an explicit call from the browser script to the API script to send and request data using the React “useState” hook.

In Remix the loader and action functions or the browser script run on the server, and have direct access to the database, the export default function…() { runs in the browser.

Place the

  • server side files (to access the database, and process data) in the app/models directory

app/models/post.server.ts file

export async function getPosts() {
  // Get all posts from the database
  return postList;

export async function getPost(slug: string) {
  // Get a post from the database
  return post;

export async function createPost(
  post: Pick<Post, "description" | "title">
) {
  // Create the post in the database
  return result;
  • browser side files (to generate the pages) in the app/routes directory

Place the code shown below into the route. loader() and action() functions do not work in the components. 

app/routes/posts.admin.tsx file

// Reference to the database access script
import { getPosts, getPost, createPost } from "~/models/post.server";

// Server side code to call the
//   getPosts() function to read data from the database
export const loader = async () => {
  return json({ posts: await getPosts() });

// Server side code to
//   receive the form data from the browser,
//   validate it,
//   and call the createPost() function
//   to save the data in the database 
export const action = async ({ request }: ActionArgs) => {

  const formData = await request.formData();

  const title = formData.get("title");
  const description = formData.get("description");

  // Error checking
  const errors = {
    title: title ? null : "Title is required",
  const hasErrors = Object.values(errors).some(
    (errorMessage) => errorMessage
  if (hasErrors) {
    return json(errors);

  await createPost({ title, description });

  return redirect("/posts/admin");


// Browser side code
export default function PostAdmin() {

  // Remix hook receive data from the "loader()" function
  const { posts } = useLoaderData<typeof loader>();

  // Remix hook to send the form data to the "action()" function
  const errors = useActionData<typeof action>();

  // Generate the HTML
  return (
            {posts.map((post) => (
              <li key={post.slug}>

      <Form method="post">
          <input type="text" name="title" />
          <input type="text" name="description" />
          <button type="submit" >Submit</button>

For more information see  Migrating your React Router App to Remix

Leave a comment

Your email address will not be published. Required fields are marked *