[Comprehensive Explanation] Remix Tutorial (Short Version)

【徹底解説】Remixのチュートリアル(short編)

The Remix tutorial uses TypeScript to guide you through the implementation, but of course it is possible to follow along using only JavaScript without using TypeScript.

I'm not that familiar with TypeScript, so I decided to work through the tutorial using JavaScript this time.

This article is not a translation of a tutorial, so I will proceed based on my own interpretation and add explanations as I go along.
If you are interested in the original text, please read it.

Prerequisites

The tutorial also includes an introduction to Gitpod , but this time we will be working through the tutorial in our own local environment.

At the time of writing this blog, the device used is a MacBook Pro running Monterey 12.5.

Also, Remix is ​​a full-stack framework based on React.js,

  • Basic knowledge of JavaScript
  • Basic knowledge of React.js

This offer is available to those who have the following:

Other prerequisites set by Remix are as follows:

  • Node.js is installed (version: 14.xx or 16.0.0 or higher)
  • npm (version: 7 or higher)
  • A code editor (such as VSCode )

Tutorial Overview

This tutorial is not about creating an app from scratch, but about using the Remix CLI. Stacks The aim seems to be to use this feature to create a fairly complete app, and then add features to it, in order to get an overview of Remix.

If you want to learn more about Remixes, try our Long Remix tutorial.

In this tutorial, we use the following source code as a template:

For this

  • Posts list page
  • Post details page
  • Post creation page

As you add more, you'll get an idea of ​​what the remix will look like.

In this template I am using Tailwild CSS is set up and used in the source code.

procedure

Create a project

Let's start creating a project right away. As mentioned above, we will use a template to create the project environment.

In the terminal, enter the following command in any directory (you can change blog-tutorial to any name you like).

 npx create -remix@latest --template remix-run/indie-stack blog-tutorial

Once the installation begins, you will be asked a few questions. Enter the information as shown below to set up your JavaScript environment.

 Need to install the following packages:
 create -remix@ 1.19 .3
 Ok to proceed? (y) y

 ? TypeScript or JavaScript? JavaScript
 
? Do you want me to run `npm install` ? Yes

Since we are using a template this time, npm install After npx remix init will be executed, and database setup will also be performed at the same time.

If you want to know more about how this works, check out the Remix documentation.

This completes the environment setup. It's easy.

You can also create your own Remix templates, so development can proceed more efficiently by turning frequently used remixes into templates.

Add a post list page

We will learn how to add a page in Remix by first creating a posts listing page.

Before creating the page, let's add a link to the page we're about to create to the top page. app/routes/_index.jsx Add the following code to your .

< div className = "mx-auto mt-16 max-w-7xl text-center" >
 < Link
 to = "/posts"
 className = "text-xl text-blue-600 underline"
 >
 Blog Posts
 </ Link >
 </ div >

You can add it anywhere you like, but to keep it consistent with the tutorial content, I placed it between the image on the top page and the items listing the technologies used.

Let's start the server and check the screen. To start the server, enter the following in the terminal: npm run dev Enter the

npm run dev

localhost:3000 Try accessing it, and if it displays as shown below, then it's OK.

 import { Link } from "@remix-run/react" ;

 import { useOptionalUser } from "~/utils" ;

 export const meta = () => [{ title: "Remix Notes" }];

 export default function Index() {
 const user = useOptionalUser();
 return ( 
<main className= "relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center" >
 <div className= "relative sm:pb-16 sm:pt-8" >
 <div className= "mx-auto max-w-7xl sm:px-6 lg:px-8" >
 <div className= "relative shadow-xl sm:overflow-hidden sm:rounded-2xl" >
 <div className= "absolute inset-0" >
 <img
 className= "h-full w-full object-cover"
 src= "https://user-images.githubusercontent.com/1500684/157774694-99820c51-8165-4908-a031-34fc371ac0d6.jpg"
 alt= "Sonic Youth On Stage"
 />

 <div className= "absolute inset-0 bg-[color:rgba(254,204,27,0.5)] mix-blend-multiply" /> 
</div>
 <div className= "relative px-4 pb-8 pt-16 sm:px-6 sm:pb-14 sm:pt-24 lg:px-8 lg:pb-20 lg:pt-32" >
 <h1 className= "text-center text-6xl font-extrabold tracking-tight sm:text-8xl lg:text-9xl" >
 <span className= "block uppercase text-yellow-500 drop-shadow-md" >
 Indie Stack
 </span>
 </h1>
 <p className= "mx-auto mt-6 max-w-lg text-center text-xl text-white sm:max-w-3xl" >
 Check the README.md file for instructions on how to get this
 Project deployed.
 </p>
 <div className= "mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center" > 
{user ? (
 <Link
 to= "/notes"
 className= "flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8"
 >
 View Notes for {user.email}
 </Link>
 ) : (
 <div className= "space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0" >
 <Link
 to= "/join"
 className= "flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8"
 >
 Sign up 
</Link>
 <Link
 to= "/login"
 className= "flex items-center justify-center rounded-md bg-yellow-500 px-4 py-3 font-medium text-white hover:bg-yellow-600"
 >
 Log In
 </Link>
 </div>
 )}
 </div>
 <a href= "https://remix.run" >
 <img
 src= "https://user-images.githubusercontent.com/1500684/158298926-e45dafff-3544-4b69-96d6-d3bcc33fc76a.svg"
 alt= "Remix"
 className= "mx-auto mt-16 w-full max-w-[12rem] md:max-w-[16rem]"
 />
 </a>
 </div>
 </div>
 </div>
 
<div className= "mx-auto mt-16 max-w-7xl text-center" >
 <Link
 to= "/posts"
 className= "text-xl text-blue-600 underline"
 >
 Blog Posts
 </Link>
 </div>

 <div className= "mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8" >
 <div className= "mt-6 flex flex-wrap justify-center gap-8" >
 {[
 {
 src: "https://user-images.githubusercontent.com/1500684/157764397-ccd8ea10-b8aa-4772-a99b-35de937319e1.svg" ,
 alt: "Fly.io" ,
 href: "https://fly.io" ,
 },
 { 
src: "https://user-images.githubusercontent.com/1500684/157764395-137ec949-382c-43bd-a3c0-0cb8cb22e22d.svg" ,
 alt: "SQLite" ,
 href: "https://sqlite.org" ,
 },
 {
 src: "https://user-images.githubusercontent.com/1500684/157764484-ad64a21a-d7fb-47e3-8669-ec046da20c1f.svg" ,
 alt: "Prisma" ,
 href: "https://prisma.io" ,
 },
 {
 src: "https://user-images.githubusercontent.com/1500684/157764276-a516a239-e377-4a20-b44a-0ac7b65c8c14.svg" ,
 alt: "Tailwind" ,
 href: "https://tailwindcss.com" ,
 },
 { 
src: "https://user-images.githubusercontent.com/1500684/157764454-48ac8c71-a2a9-4b5e-b19c-edef8b8953d6.svg" ,
 alt: "Cypress" ,
 href: "https://www.cypress.io" ,
 },
 {
 src: "https://user-images.githubusercontent.com/1500684/157772386-75444196-0604-4340-af28-53b236faa182.svg" ,
 alt: "MSW" ,
 href: "https://mswjs.io" ,
 },
 {
 src: "https://user-images.githubusercontent.com/1500684/157772447-00fccdce-9d12-46a3-8bb4-fac612cdc949.svg" ,
 alt: "Vitest" ,
 href: "https://vitest.dev" ,
 },
 { 
src: "https://user-images.githubusercontent.com/1500684/157772662-92b0dd3a-453f-4d18-b8be-9fa6efde52cf.png" ,
 alt: "Testing Library" ,
 href: "https://testing-library.com" ,
 },
 {
 src: "https://user-images.githubusercontent.com/1500684/157772934-ce0a943d-e9d0-40f8-97f3-f464c0811643.svg" ,
 alt: "Prettier" ,
 href: "https://prettier.io" ,
 },
 {
 src: "https://user-images.githubusercontent.com/1500684/157772990-3968ff7c-b551-4c55-a25c-046a32709a8e.svg" ,
 alt: "ESLint" ,
 href: "https://eslint.org" ,
 },
 { 
src: "https://user-images.githubusercontent.com/1500684/157773063-20a0ed64-b9f8-4e0b-9d1e-0b65a3d4a6db.svg" ,
 alt: "TypeScript" ,
 href: "https://typescriptlang.org" ,
 },
 ].map( (img) => (
 <a
 key={img.href}
 href={img.href}
 className= "flex h-16 w-32 justify-center p-1 grayscale transition hover:grayscale-0 focus:grayscale-0"
 >
 <img alt={img.alt} src={img.src} className= "object-contain" />
 </a>
 ))}
 </div>
 </div>
 </div>
 </main>
 );
 }

Continue app/routes/posts._index.jsx Create a

touch app/routes/posts._index.jsx

Edit the content as follows:

 export default function Posts ( ) {
 return (
 < main >
 < h1 > Posts </ h1 >
 </ main >
 );
 }

localhost:3000/posts If you access it and see a page with just the word Posts, then you've succeeded. You've successfully created a new page using Remix.

In this way, Remix allows you to add a new page simply by adding a file.

This function, which automatically creates URL paths by creating a file that follows certain rules, is called file-based routing .

For details on naming rules in Remix, see Route File Naming (v2) .

Completion of the post list page

Currently the post list page only displays the word "Posts", but ideally we would like to retrieve information from a database via a backend API and display that information on the screen.

In Remix, you define a loader function to add a backend API for displaying a page. For example, to add a backend API for the post list page, use app/routes/posts._index.jsx Modify it as follows:

 import { json } from "@remix-run/node" ;
 import { useLoaderData } from "@remix-run/react" ;

 export const loader = async () => { 
return json({
 posts : [
 {
 slug : "my-first-post" ,
 title : "My First Post" ,
 },
 {
 slug : "90s-mixtape" ,
 title : "A Mixtape I Made Just For You" ,
 },
 ],
 });
 };

 export default function Posts ( ) {
 const { posts } = useLoaderData();
 return (
 < main > 
< h1 > Posts </ h1 >
 </ main >
 );
 }

To get the added backend API information, use a custom hook called userLoaderData in the frontend code. In the above code, the contents of posts are extracted from the backend API data and assigned to the constant posts .

useLoaderData

Next, app/routes/posts._index.jsx Let's modify the contents as follows and display the information obtained from the backend on the screen.

 import { json } from "@remix-run/node" ;
 // Linkコンポーネントをインポート 
import { Link, useLoaderData } from "@remix-run/react" ;

 // ...

 export default function Posts ( ) {
 const { posts } = useLoaderData();
 // Modify to use backend data
 return (
 < main >
 < h1 > Posts </ h1 >
 <ul>
 {posts.map((post) => ( 
< li key = {post.slug} >
 < Link
 to = {post.slug}
 className = "text-blue-600 underline"
 >
 {post.title}
 </ Link >
 </ li >
 ))}
 </ul>
 </ main >
 );
 }

localhost:3000/posts When you access the page, the data sent from the backend is displayed on the screen as shown below.

 import { json } from "@remix-run/node" ;
 import { Link, useLoaderData } from "@remix-run/react" ;

 export const loader = async () => {
 return json({
 posts : [
 {
 slug : "my-first-post" , 
title : "My First Post" ,
 },
 {
 slug : "90s-mixtape" ,
 title : "A Mixtape I Made Just For You" ,
 },
 ],
 });
 };

 export default function Posts ( ) {
 const { posts } = useLoaderData();
 return (
 < main >
 < h1 > Posts </ h1 >
 <ul> 
{posts.map((post) => (
 < li key = {post.slug} >
 < Link
 to = {post.slug}
 className = "text-blue-600 underline"
 >
 {post.title}
 </ Link >
 </ li >
 ))}
 </ul>
 </ main >
 );
 }

By the way, the code in the routes is getting a bit cluttered, so it might be a good idea to split each process into separate files according to their responsibilities.

Typically, data behavior is handled by models , so app/models In the directory app/models/post.server.js Let's deal with this by adding:

 touch app/models/post.server.js

app/routes/posts._index.jsx We will create a getPosts function that directly reflects the contents of the loader function and modify it to call that function.

 // app/models/post.server.js
 
export async function getPosts ( ) {
 return [
 {
 slug : "my-first-post" ,
 title : "My First Post" ,
 },
 {
 slug : "90s-mixtape" ,
 title : "A Mixtape I Made Just For You" ,
 },
 ];
 }
 // app/routes/posts._index.jsx

 // 追加
import { getPosts } from "~/models/post.server" ;

 // 内容を変更 
export const loader = async () => {
 return json({ posts: await getPosts() });
 };

Now, the post list screen is complete using the data received from the model.

When importing the getPosts function ~ I used the character .

this is tsconfig.json The value set in the app/* is used for ease of reference.

reference:

💡 Naming in app/modes/ .server I added the extension.

This instructs the compiler to ignore modules and their imports in files that contain this extension in their filenames when bundling JavaScript code for the browser.

reference: https://remix.run/docs/en/main/guides/constraints#no-module-side-effects

Using the Prisma Database

Since we created the environment using a template, SQLite and Prisma are already set up in this project.

We'll make a few changes to the posts page so that it displays data from the database.

prisma/schema.prisma Add the following to the end of

 model Post {
 slug String @id
 title String
 markdown String

 createdAt DateTime @default(now())
 updatedAt DateTime @updatedAt
 }

Next, execute the following command to reflect the changes in the database, which will add a new Post table to the database.

 npx prisma migrate dev --name "create post model"

Also, to add initial data to Post Add the following to prisma/seed.ts , then in the terminal npx prisma db seed Run to reflect the data.

 const posts = [
 {
 slug: "my-first-post",
 title: "My First Post",
 markdown: `
 # This is my first post

 Isn't it great?
 `.trim(),
 },
 {
 slug: "90s-mixtape",
 title: "A Mixtape I Made Just For You",
 markdown: `
 # 90s Mixtape
 
- I wish (Skee-Lo)
 - This Is How We Do It (Montell Jordan)
 - Everlong (Foo Fighters) 
- Ms. Jackson (Outkast)
 - Interstate Love Song (Stone Temple Pilots)
 - Killing Me Softly With His Song (Fugees, Ms. Lauryn Hill)
 - Just a Friend (Biz Markie)
 - The Man Who Sold The World (Nirvana)
 - Semi-Charmed Life (Third Eye Blind)
 - ...Baby One More Time (Britney Spears)
 - Better Man (Pearl Jam)
 - It's All Coming Back to Me Now (Céline Dion)
 - This Kiss (Faith Hill)
 - Fly Away (Lenny Kravits)
 - Scar Tissue (Red Hot Chili Peppers)
 - Santa Monica (Everclear)
 - C'mon N' Ride it (Quad City DJ's)
 `.trim(),
 },
 ];
 
for (const post of posts) {
 await prisma.post.upsert({
 where: { slug: post.slug },
 update: post,
 create: post,
 });
 }

 // Add it just before this
 console.log( `Database has been seeded. 🌱` );
 npx prisma db seed

lastly, app/models/post.server.js Change the contents of the getPosts function to retrieve data from the database via Prisma and you're done.

 import { prisma } from "~/db.server" ;

 export async function getPosts ( ) { 
return prisma.post.findMany();
 }

We will not be explaining the code using Prisma this time, so if you are interested, please check the documentation on the official website.

Prisma Documentation | Concepts, Guides, and Reference

Add a post details page

Next, we will create a post detail page. The post detail page uses the post ID (slug column of the Post table) in the URL (e.g.: /posts/my-first-post …where my-first-post is the post ID)

In this way, we use Remix's dynamic segment feature to be able to set any value instead of a fixed URL.

To use a dynamic segment, add $ So, the file name for the post details page we're creating is posts.$slug.jsx Let's assume that.

 touch app/routes/posts.\\ $slug .jsx

As in the example, keep the contents of the file simple and check that the page has been created correctly.

export default function PostSlug ( ) {
 return (
 < main className = "mx-auto max-w-4xl" >
 < h1 className = "my-6 border-b-2 text-center text-3xl" >
 Some Post
 </ h1 >
 </ main >
 );
 }

If there are no errors, we will create the content of the details page.

app/routes/posts.$slug.jsx Edit the content as follows:

 import { json } from "@remix-run/node" ;
 import { useLoaderData } from "@remix-run/react" ;

 export const loader = async ({ params }) => {
 return json({ slug : params.slug });
 };

 export default function PostSlug ( ) {
 const { slug } = useLoaderData();
 return ( 
< main className = "mx-auto max-w-4xl" >
 < h1 className = "my-6 border-b-2 text-center text-3xl" >
 Some Post: {slug}
 </ h1 >
 </ main >
 );
 }

The values ​​set for the dynamic segments can be obtained from the params argument passed to the loader function .

For example, in the above example, $slug Because the dynamic segment is set params.slug The value is obtained by calling

The following code has been modified to perform a search in the database based on this value and display the post title on the screen.

 // app/models/post.server.js

 import { prisma } from "~/db.server" ;

 export async function getPosts ( ) {
 return prisma.post.findMany();
 }

 export async function getPost ( slug: string ) { 
return prisma.post.findUnique({ where : { slug } });
 }
 // app/routes/posts.$slug.jsx

 import { json } from "@remix-run/node" ;
 import { useLoaderData } from "@remix-run/react" ;

 import { getPost } from "~/models/post.server" ;

 export const loader = async ({ params }) => {
 const post = await getPost(params.slug);
 return json({ post });
 };
 
export default function PostSlug ( ) {
 const { post } = useLoaderData();
 return (
 < main className = "mx-auto max-w-4xl" >
 < h1 className = "my-6 border-b-2 text-center text-3xl" >
 {post.title}
 </ h1 >
 </ main >
 );
 }

Since this article uses JavaScript, the "TypeScript warning" mentioned in the tutorial does not appear, but in case the dynamic segment value is not passed or the data does not exist in the database, tiny-invaliant Let's add a simple value check using

 // ...

 import invariant from "tiny-invariant" ;

 export const loader = async ({ params }) => {
 invariant(params.slug, "params.slug is required" );

 const post = await getPost(params.slug); 
invariant(post, `Post not found: ${params.slug} ` );

 return json({ post });
 };

 // ... 

The invariant function of tiny-invaliant behaves depending on whether the value passed to the first argument is a true value or a false value.

If the value is true , nothing will be done; if the value is false , an exception will be raised with the value passed in the second argument as an error message and the process will be terminated.

reference: https://www.npmjs.com/package/tiny-invariant

Finally, we want the detail page to display not only the post title, but also the content (markdown column) values.

To convert markdown to HTML, the tutorial uses the marked library .

After adding the library, restart the server. app/routes/posts.$slug.jsx Change the content to show the mergedown, and your post details page is complete.

 npm add marked
 import { json } from "@remix-run/node" ;
 import { useLoaderData } from "@remix-run/react" ;
 import { marked } from "marked" ; 
import invariant from "tiny-invariant" ;

 import { getPost } from "~/models/post.server" ;

 export const loader = async ({ params }) => {
 invariant(params.slug, "params.slug is required" );

 const post = await getPost(params.slug);
 invariant(post, `Post not found: ${params.slug} ` );

 const html = marked(post.markdown);
 return json({ html, post });
 };
 
export default function PostSlug ( ) {
 const { html, post } = useLoaderData();
 return (
 < main className = "mx-auto max-w-4xl" >
 < h1 className = "my-6 border-b-2 text-center text-3xl" >
 {post.title}
 </ h1 > 
< div dangerouslySetInnerHTML = {{ __html: html }} />
 </ main >
 );
 }

localhost:3000/posts/90s-mixtape If you access it and see the following, then everything is perfect (it is not decorated with CSS because the Tailwind CSS reset CSS has been applied).

Add a new post page

Finally, we will learn about backend integration using nested routing , index routes , and action functions while adding a new post page.

First, let's add a new admin page and add a link to it on the posts index page.

 // app/routes/posts._index.jsx

 // これの下に追加
< h1 > Posts </ h1 >

 < Link to = "admin" className = "text-red-600 underline" >
 Admin
 </ Link >
 touch app/routes/posts.admin.jsx
 // app/routes/posts.admin.jsx

 import { json } from "@remix-run/node" ; 
import { Link, useLoaderData } from "@remix-run/react" ;

 import { getPosts } from "~/models/post.server" ;

 export const loader = async () => {
 return json({ posts : await getPosts() });
 };

 export default function PostAdmin ( ) {
 const { posts } = useLoaderData();
 return ( 
< div className = "mx-auto max-w-4xl" >
 < h1 className = "my-6 mb-2 border-b-2 text-center text-3xl" >
 Blog Admin
 </ h1 >
 < div className = "grid grid-cols-4 gap-6" >
 < nav className = "col-span-4 md:col-span-1" >
 <ul>
 {posts.map((post) => ( 
< li key = {post.slug} >
 < Link
 to = {post.slug}
 className = "text-blue-600 underline"
 >
 {post.title}
 </ Link >
 </ li >
 ))}
 </ul>
 </ nav > 
< main className = "col-span-4 md:col-span-3" >
 ...
 </ main >
 </ div >
 </ div >
 );
 }

The points worth mentioning are app/routes/posts._index.jsx This is the Link component added to.

to=”/posts/admin” instead to="admin" Even though it says "Admin", when you click localhost:3000/posts/admin will transition to.

This is because Remix's Link component can use relative paths. It's easier to understand how to use it if you imagine the Linux command cd .

Remix's Link component is a wrapper around React Router , so if you want to learn more, check out the React Router documentation.

Next, let's add a new post form to the admin page. We'll define the new post form as a child route (component) using Remix's nested routing feature.

This article focuses on how to use nested routing.

Please refer to the official documentation for a detailed explanation of the concepts.

reference: https://remix.run/docs/en/main/guides/routing

First, add an index route that corresponds to the admin page. The simple explanation of the index route is "the default display when displaying child components", and the file name is ._index This refers to anything that includes.

 touch app/routes/posts.admin._index.jsx
 // app/routes/posts.admin._index.jsx

 import { Link } from "@remix-run/react" ;

 export default function AdminIndex ( ) {
 return ( 
<p>
 < Link to = "new" className = "text-blue-600 underline" >
 Create a New Post
 </ Link >
 </ p >
 );
 }

To display child components using nested routing , we use a component called Outlet . app/routes/posts.admin.jsx Change it to show the outlet .

import { json } from "@remix-run/node" ;
 // Outletを追加
import { Link, Outlet, useLoaderData } from "@remix-run/react" ;

 import { getPosts } from "~/models/post.server" ;

 export const loader = async () => {
 return json({ posts : await getPosts() });
 };
 
export default function PostAdmin ( ) {
 const { posts } = useLoaderData();
 return (
 < div className = "mx-auto max-w-4xl" >
 < h1 className = "my-6 mb-2 border-b-2 text-center text-3xl" >
 Blog Admin
 </ h1 >
 < div className = "grid grid-cols-4 gap-6" > 
< nav className = "col-span-4 md:col-span-1" >
 <ul>
 {posts.map((post) => (
 < li key = {post.slug} >
 < Link
 to = {post.slug}
 className = "text-blue-600 underline"
 >
 {post.title}
 </ Link >
 </ li >
 ))} 
</ul>
 </ nav >
 < main className = "col-span-4 md:col-span-3" >
 {/* Change to show outlet */}
 < Outlet />
 </ main >
 </ div >
 </ div >
 );
 }

localhost:3000/posts/admin Go to <Outlet /> to the index root ( posts.admin._index.jsx ) is displayed.

Now we need to display the form for new posts in this <Outlet /> and the layout of the admin page is complete.

 touch app/routes/posts.admin.new.jsx
 import { Form } from "@remix-run/react";

 const inputClassName =
 "w-full rounded border border-gray-500 px-2 py-1 text-lg";

 export default function NewPost() {
 return (
 < Form method = "post" >
 < p > 
< label >
 Post Title:{" "}
 < input
 type = "text"
 name = "title"
 className = {inputClassName}
 />
 </ label >
 </ p >
 <p>
 < label >
 Post Slug:{" "}
 < input
 type = "text" 
name = "slug"
 className = {inputClassName}
 />
 </ label >
 </ p >
 <p>
 < label htmlFor = "markdown" > Markdown: </ label >
 <br />
 < textarea
 id = "markdown"
 rows = {20} 
name = "markdown"
 className = { `${ inputClassName } font-mono `}
 />
 </ p >
 < p className = "text-right" >
 < button
 type = "submit"
 className = "rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
 >
 Create Post
 </ button > 
</ p >
 </ Form >
 );
 }

localhost:3000/posts/admin/new will look like this:

Saving a new post from the New Post form

In the current state, an error will occur when submitting information through the new submission form.

In order to save this information in a database or similar, you will need to prepare a backend API that supports forms.

In Remix, you can define an action function to prepare a backend API that supports forms like this. The action function is written directly in the routes file, just like the loader function described earlier in the tutorial.

app/models/post.server.js and app/routes/posts.admin.new.jsx Add the following code to your .

 // app/models/post.server.js

 // ..

 // Postテーブルにデータを保存するための記述
export async function createPost ( post ) {
 return prisma.post.create({ data : post });
 }
 // app/routes/posts.admin.new.jsx

 // ...

 import { redirect } from "@remix-run/node" ; 
import { createPost } from "~/models/post.server" ;

 export const action = async ({ request }) => {
 const formData = await request.formData();

 const title = formData.get( "title" );
 const slug = formData.get( "slug" );
 const markdown = formData.get( "markdown" );

 await createPost({ title, slug, markdown });

 return redirect( "/posts/admin" );
 };

 // ...

This completes the process of registering the post in the database.

Since we are not using TypeScript in this article, we have not made any corrections to the TypeScript warnings.

Therefore, there are some differences between the original tutorial and the source code.

💡 The request and formData used in the action function are web standards, so if you want to know the detailed specifications, please check MDN.

reference:

Adding Validation

This concludes the blog tutorial!

Well, there's more to it than that. Specifically, we'll add validation to the user's input and display error messages.

app/routes/posts.admin.new.jsx Let's modify the action function so that it returns an error response if any values ​​are not entered.

 // ...

 // jsonを追加
import { json, redirect } from "@remix-run/node" ;

 export const action = async ({ request }) => {
 const formData = await request.formData();

 const title = formData.get( "title" ); 
const slug = formData.get( "slug" );
 const markdown = formData.get( "markdown" );

 // If the value is an empty string, add a process to return an error message in JSON format.
 const errors = {
 title: title ? null : "Title is required" ,
 slug: slug ? null : "Slug is required" ,
 markdown: markdown ? null : "Markdown is required" ,
 };
 const hasErrors = Object .values(errors).some(
 ( errorMessage ) => errorMessage
 );
 if (hasErrors) { 
return json(errors);
 }

 await createPost({ title, slug, markdown });

 return redirect( "/posts/admin" );
 };

 // ...

If you press the submit button without entering anything in the input form, a response containing an error message will be returned and the data will not be saved.

Below is the content of the communication confirmed by the Google Chrome extension. You can see that an error message is returned.

We use useActionData to get this error message and display it on the screen.

 // useActionDataを追加
import { Form, useActionData } from "@remix-run/react";

 // ...

 export default function NewPost() {
 // エラーメッセージのレスポンスを取得
 const errors = useActionData();

 return ( 
< Form method = "post" >
 <p>
 < label >
 Post Title:{" "}
 {/* Add a process to display an error message if one exists */}
 {errors?.title? (
 < em className = "text-red-600" > {errors.title} </ em >
 ) : null} 
< input type = "text" name = "title" className = {inputClassName} />
 </ label >
 </ p >
 <p>
 < label >
 Post Slug:{" "}
 {/* Add a process to display an error message if one exists */}
 {errors?.slug ? ( 
< em className = "text-red-600" > {errors.slug} </ em >
 ) : null}
 < input type = "text" name = "slug" className = {inputClassName} />
 </ label >
 </ p >
 <p>
 < label htmlFor = "markdown" > 
Markdown: {" "}
 {/* Add a process to display an error message if one exists */}
 {errors?.markdown?(
 < em className = "text-red-600" >
 {errors.markdown}
 </em>
 ) : null}
 </ label >
 <br />
 < textarea
 id = "markdown"
 rows = {20}
 name = "markdown" 
className = { `${ inputClassName } font-mono `}
 />
 </ p >
 < p className = "text-right" >
 < button
 type = "submit"
 className = "rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
 >
 Create Post
 </ button >
 </ p > 
</ Form >
 );
 }

So, this concludes the coding part of the tutorial. Thank you for your hard work!

About the assignment

There are some challenges at the end of the Remix tutorial, but we won't be explaining them this time.

About Progressive Enhancement

Finally, I would like to touch on progressive enhancement, which is an important concept of remix.

This philosophy is based on the idea of ​​"Let's create an app that functions at a minimum level for people in any environment." For example, this is a design philosophy that involves creating an app that functions at a minimum level even when used by users who cannot use JavaScript (or have JavaScript disabled), while providing a better user experience for people in environments where all functions are available.

💡 If you want to know more about the idea of ​​remix and progressive enhancement, check out the following pages:

reference:

Try it out

I mentioned earlier that the coding for this tutorial is complete, but let's take this opportunity to try out progressive enhancement.

app/routes/posts.admin.new.jsx Please add the following code to your browser, disable JavaScript, and then create a new post.

You can see that the site works properly whether JavaScript is enabled or disabled, but the content provided changes depending on the user's environment.

 // useNavigationを追加
import { Form, useActionData, useNavigation } from "@remix-run/react" ;

 // ...

 export const action = async ({ request }: ActionArgs) => { 
// Add a process to wait 3 seconds
 await new Promise ( ( res ) => setTimeout(res, 3000 ));

 // ...

 export default function NewPost ( ) {
 const errors = useActionData();

 // Define a flag to indicate whether data is being sent
 const navigation = useNavigation();
 const isCreating = Boolean (
 navigation.state === "submitting"
 );
 
return (
 < Form method = "post" >
 {/* ... */}

 {/* If JavaScript is enabled, change the notation of the send button to Creating... */}
 < p className = "text-right" >
 < button
 type = "submit"
 className = "rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
 disabled = {isCreating}
 >
 {isCreating ? "Creating..." : "Create Post"} 
</ button >
 </ p >
 </ Form >
 );
 }

summary

So far, we have been working through the JavaScript Remix tutorial (shot).

As you'll see when you go through the long tutorial, the functions introduced here are just a small part of it, and Remix has many more useful features, so we encourage you to give it a try!

Also, this time I used JavaScript because I am not familiar with TypeScript, but to be honest, the writing style is similar in both cases, so I felt that studying and adopting TypeScript was a better choice in terms of "reducing errors due to typing."

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.