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

Change 

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

to

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

Routing

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
  href="/about"
  rel="noreferrer"
>
  About
</a>

<a target="_blank"
  href="https://remix.run/docs"
  rel="noreferrer">
  Remix Docs
</a>

Images

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.

<Image
  src="my-logo.png"
  responsive={[
    {
      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">
      <head>
        ...
        <Links />
        ...
      </head>
      <body>
        ...
      </body>
    </html>
  );
}

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 */}
      <Header/>

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

      {/* Display the Footer component */}
      <Footer/>
    </>
  )
}

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">
      <ClientOnly>
        <Select />
      </ClientOnly>
    </div>
  )
}

Application logic

IMPORTANT:

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 (
      <div>
          <ul>
            {posts.map((post) => (
              <li key={post.slug}>
                {post.title}
              </li>
            ))}
          </ul>
      </div>

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

For more information see  Migrating your React Router App to Remix

Cannot edit table rows in pgAdmin 4

If the table has no primary key, pgAdmin 4 cannot identify them, so disables the row insert and edit. Lock icons appear next to the column names, and the Add row button is disabled.

To be able to edit table rows in pgAdmin4, select the primary key column of the table.

  • Close the row editor window, as we will update the table properties
  • Right-click the table name and select Properties
  • On the Columns tab select a column as the Primary key, and click the Save button
  • Right-click the table name and select View/Edit Data, All Rows
  • The pencil icons show, that the data is editable, and the Add row button is available.

How to expose Next.js environment variables to the browser specific code

The Next.js React web application runs separate code on the server and in the browser. By default, the environment variables are only accessible in the server side script.

Warning: by exposing environment variables to the browser code you may reveal secrets. Make sure the exposed environment variables do not contain any secrets. If secrets are needed in the browser code, create an API page in the pages/api directory and fetch it from the browser code.

To use environment variables in the browser side script,

  • expose those in the next.config.js file
module.exports = {
  ...
  
  env: {
    // declare here all your variables
    MY_ENVIRONMENT_VARIABLE: process.env.MY_ENVIRONMENT_VARIABLE,
  }
};
  • Restart the server for the change to take effect !!!
  • Read the environment variable value from process.env in the browser specific file.
<div className={styles.footer} >
  Version: '2023-06-22_01' &nbsp;{process.env.ENVIRONMENT}
</div>

Error: Cannot find module ‘…’ outside of the development environment

When the Node.js application runs correctly in the development environment, but throws the following error in higher environments:

Error: Cannot find module ‘…’

Make sure the module is not listed in the “devDependencies” section of the package.json and package-lock.json files.

If you have installed a package with the --save-dev @types/... option, the package is considered a development-only tool, and will not be compiled into the production code.

To correct the issue:

  • If you use the Makefile to list all your installed packages, remove the --save-dev @types/... option from the npm install line
  • Uninstall the package with npm uninstall MY_PACKAGE_NAME
  • Install the package again with npm install but, without the --save-dev @types/MY_PACKAGE_NAME option
    The package.json and package-lock.json files are automatically updated by npm.
  • Rebuild and deploy the application.

error: INSERT has more expressions than target columns

When the parameterized PostgreSQL statement has more elements in the “VALUES” option than the number of columns specified, we get the error message:

error: INSERT has more expressions than target columns

In the sample code below there are two columns listed, and there are three elements in the “VALUES” option.

      const query = "INSERT INTO my_table (name, description) VALUES ($1, $2, $3)";

Solution

Remove the extra elements from the “VALUES” option.

error: could not determine data type of parameter $1

For security reasons we should always use parameterized queries to avoid SQL injections through parameters.

The usual format is

      const query = "INSERT INTO my_table (name, description) VALUES ($2, $3)";
      const parameters = ['not_used', 'my-name', 'my-description'];

When a PostgreSQL command runs and a parameter is not used, we get the error message

error: could not determine data type of parameter $1

Solution:

Remove the unused parameter from the “parameters” array, in this case the first one.

How to disable static page generation during Next.js site build

If your Next.js React TypeScript web application uses database calls to display data on the pages, and during build time the database is not available, we need to disable the static page generation, otherwise we get the following error:

info Generating static pages

TypeError: Failed to parse URL from …

code: ‘ERR_INVALID_URL’

To disable the static page generation during build, Add the getInitialProps function to the App object in the pages/_app.tsx file.

import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {

  return <Component {...pageProps} />
}

App.getInitialProps = async ({}) => {
  return {  }
}

 

How to disable linting during React Next.js Node.js application build

If your application is in the proof of concept phase, it works and you just want to build it, you can disable the linting process to have a successful build regardless of linting errors.

  • To disable TSLint for one line only, add the // @ts-ignore before the line.
  • To disable TSLint on the entire file add // @ts-nocheck as the first line of the file.
  • To disable ESLint during build add the DISABLE_ESLINT_PLUGIN=true environment variable to the .env file in your project root directory
  • To disable ESLint during Docker image build add the ARG DISABLE_ESLINT_PLUGIN=true to the Dockerfile

Object is possibly ‘undefined’

The ESLint static code analysis tool flags code where we operate on objects which can be undefined.

Object is possibly ‘undefined’

The end of the red line shows the extent of the object which can be undefined, as the “children” element is defined as optional in the type definition:

To satisfy the linter, we need to add another question mark after “children” to guard against undefined values.

hashTable[parent]?.children?.push(hashTable[element.id]);