Remix: Data mutations using forms and actions

Let’s add a little form to add a new blog post.

There will be no authentication here, so anyone can add a blog post.

Not something recommended to have on a public site of course, but will work for our sample app to demonstrate mutations.

Create a new route component in app/routes/blog.new.jsx so it will “respond” on the URL /blog/new.

You can link to it in the component from app/routes/blog.jsx:

//...
        <ul className='list-disc list-inside row-span-1'>
          {posts.map((post) => (
            <li key={post.id}>
              <NavLink
                to={`/blog/${post.id}`}
                className={({ isActive }) => (isActive ? 'font-bold' : '')}>
                {post.title}
              </NavLink>
            </li>
          ))}
          <li>
            <NavLink
              to='/blog/new'
              className={({ isActive }) => (isActive ? 'font-bold' : '')}>
              New post
            </NavLink>
          </li>
        </ul>
//...

Now in app/routes/blog.new.jsx start by creating a component:

export const meta = () => {
  return [{ title: 'New post' }]
}

export default function NewPost() {

  return (
    //...
  )
}

Now we create a form using the Form component that’s provided by Remix:

import { Form } from '@remix-run/react'

export const meta = () => {
  return [{ title: 'New post' }]
}

export default function NewPost() {
  return (
    <Form method='post'>
      <label>
        Title: <input className='border' name='title' required='true' />
      </label>
      <button type='submit' className='p-2 bg-zinc-300 border'>
        Create post
      </button>
    </Form>
  )
}

When the form is submitted, Remix executes the action on the route.

So we add it by adding a named function export action.

In there we first get the post title from the form data:

export async function action({ request }) {
  const formData = await request.formData()
  const title = formData.get('title')

  //...
}

Then we insert this new post in the database, and we redirect to it:

import { redirect } from '@remix-run/node'
import { getDb } from '../database.server.js'

//...

export async function action({ request }) {
  const formData = await request.formData()
  const title = formData.get('title')

  const db = await getDb()
  const result = await db.run('INSERT INTO posts (title) VALUES (?)', title)
  await db.close()

  return redirect(`/blog/${result.lastID}`)
}

Here’s the full code of the form fully working:

import { Form } from '@remix-run/react'
import { redirect } from '@remix-run/node'
import { getDb } from '../database.server.js'

export const meta = () => {
  return [{ title: 'New post' }]
}

export async function action({ request }) {
  const formData = await request.formData()
  const title = formData.get('title')

  const db = await getDb()
  const result = await db.run('INSERT INTO posts (title) VALUES (?)', title)
  await db.close()

  return redirect(`/blog/${result.lastID}`)
}

export default function NewPost() {
  return (
    <Form method='post'>
      <label>
        Title: <input className='border' name='title' required='true' />
      </label>
      <button type='submit' className='p-2 bg-zinc-300 border'>
        Create post
      </button>
    </Form>
  )
}

Lessons in this unit:

0: Introduction
1: Create your first Remix app
2: The root route
3: File based routing
4: Linking your pages
5: Styling with CSS and Tailwind
6: Create a navigation
7: Dynamic routes and nested routes
8: Connecting a database
9: ▶︎ Data mutations using forms and actions