Recently, I saw a front-end project remix. run, which felt very comfortable to use. However, when I used it, I wanted to find some information, but I found that there were few information in China and it was not systematic. Therefore, preliminary translation is considered according to the official tutorial. If you find any question, welcome to communicate The original connection: https://remix.run/docs/en/v1/tutorials/blog related other want content will continue to follow up

Check out this article on my blog

Quickstart

Here, we’ll introduce you with a short file and code, and if you want to learn all about Remix in 15 minutes, this is a great place to do it

{% note info simple %} ๐Ÿ’ฟ Hey I’m Derrick the Remix Compact Disc ๐Ÿ‘‹ You’ll see me whenever you should be doing something

We use TypeScript here, but we always validate methods after we’ve finished some code. This is not our normal workflow. In order not to mess up the code of those of you who don’t use TypeScript. Identify the code we need to use at the beginning (Measure Twice, cut once!).

Create a project

๐Ÿ’ฟ Initializes a new Remix project 1

NPX create-remix@latest # Select Remix App Server CD [the folder you named the project] NPM run devCopy the code

{% note Danger Simple %} Select Remix App Server

We’ll do some work with the file system, because not all of the Settings are compatible with the code in this tutorial.

Open https://localhost:3000 and your application should already be running. If you wish, take a moment to browse through the starter template, which has a lot of information.

If your application is not working properly at https://localhost:3000, refer to the readme.md documentation in your project to see if you need to make additional configurations.

Your first route

We’ll create a new route for “/posts”, but before we start, we’ll need to link to it.

Something happens in the file. Find the Layout component and add a new link to “/posts” after the link to “Home”

๐Ÿ’ฟ adds a link to posts in the app/root.tsx file

<li>
  <Link to="/posts">Posts</Link>
</li>
Copy the code

Go back to your browser and you’ll see a new link at the top. Click on it and you’ll see the 404 page. Now let’s create a new route for it:

๐Ÿ’ฟ create new files in the following path app/routes/posts/index. The TSX

mkdir app/routes/posts
touch app/routes/posts/index.tsx
Copy the code

{% note info simple%} Whenever you see a terminal command to create a folder or file, feel free to do so. Use mkdir and touch just to let us know which folders you should create.

We’ll call it posts.tsx, but we’ll have another route soon, and it would be nice to put them together. The indexed route will be rendered in the path of the file (just like index.html in a Web server)

You may find that the screen is empty, you have a route with nothing (at least it has NULL), let’s add a default component for it.

๐Ÿ’ฟ making posts app/component routes/posts/index. The TSX:

export default function Posts() {
  return (
    <div>
      <h1>Posts</h1>
    </div>
  );
}
Copy the code

You may need to refresh to see our new, basic routing information.

Load the data

Data loading is built into Remix.

If you’ve been doing Web development primarily in the past few years, you’ve probably been used to doing two things here: API routing that provides data and front-end components that consume it. In Remix, your front-end component is also its own API route, and it knows how to talk to the server in the browser, meaning you don’t have to fetch its data anymore.

If your background is further than Rails,PHP, etc. You can think of your Remix route as a back-end view of React modularized, but it has a flair for seamlessly adding elements to the browser. It is the most appropriate manifestation of gradual increase.

So let’s start providing some data for our component.

๐Ÿ’ฟ add “loader” to posts route

app/routes/posts/index.tsx

import { useLoaderData } from "remix";

export let loader = () = > {
  return[{slug: "my-first-post".title: "My First Post"
    },
    {
      slug: "90s-mixtape".title: "A Mixtape I Made Just For You"}]; };export default function Posts() {
  let posts = useLoaderData();
  console.log(posts);
  return (
    <div>
      <h1>Posts</h1>
    </div>
  );
}
Copy the code

Loaders is a back-end “API” for its components, which is linked for you via useLoaderData. Blurring the line between client and server in Remix routing sounds a bit crazy. However, if you open Consoles on server and browser, you will find that they all record our data. That’s because we still send the finished HTML document on Remix’s server like a traditional Web framework, but it’s also fusing and logging on the client side.

{% note info simple %} we use let because it is only three letters long, you can also use const๐Ÿ™‚ if you wish

๐Ÿ’ฟ links for updating our posts app/routes/posts/index. The TSX

import { Link, useLoaderData } from "remix";

// ...
export default function Posts() {
  let posts = useLoaderData();
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.slug}>
            <Link to={post.slug}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}
Copy the code

TypeScript is wrong, let’s fix it:

๐Ÿ’ฟ for your request to add useLoaderData app/routes/posts/index. The TSX

import { Link, useLoaderData } from "remix";

type Post = {
  slug: string;
  title: string;
};

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

export default function Posts() {
  let posts = useLoaderData<Post[]>();
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.slug}>
            <Link to={post.slug}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}
Copy the code

Hey, that’s cool. Even with network requests we can have pretty solid type safety because they’re all defined in the same file. You are type-safe in this component, and it is API(remember, this component is already its own API route), unless the network crashes while Remix is fetching data.

A little refactoring

A safe bet is to create a module that handles a particular problem. Our example involves reading and editing articles. Now let’s update it without adding a getPosts to our module

๐Ÿ’ฟ create app/post. Ts

touch app/post.ts
Copy the code

We mainly copied/pasted the following from our route: app/post.ts

export type Post = {
  slug: string;
  title: string;
};

export function getPosts() {
  let posts: Post[] = [
    {
      slug: "my-first-post".title: "My First Post"
    },
    {
      slug: "90s-mixtape".title: "A Mixtape I Made Just For You"}];return posts;
}
Copy the code

๐Ÿ’ฟ update our route so that he could use a new module app/routes/posts/index. The TSX

import { Link, useLoaderData } from "remix";
import { getPosts } from "~/post";
import type { Post } from "~/post";

export let loader = () = > {
  return getPosts();
};

// ...
Copy the code

Get data from the data source

If we really want to build it, we hope that our data is a database, such as the Postgres, FaunaDB, Supabase and so on. But this is just a quick start, we’ll just use file systems.

Instead of hard-coding the data, we read it from the file system.

๐Ÿ’ฟ create a “posts/” folder in the root directory, not in the app path, but next to it.

mkdir posts
Copy the code

Now create more posts

touch posts/my-first-post.md
touch posts/90s-mixtape.md
Copy the code

Put whatever you want inside them, but make sure they have the title property

posts/my-first-post.md

---
title: My First Post
---

# This is my first post

Isn't it great?
Copy the code

posts/90s-mixtape.md

--- title: 90s Mixtape --- # 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 (Celine 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)Copy the code

๐Ÿ’ฟ updates getPosts so that they can be read from the file

To do this, we need to add a new module:

npm add front-matter
Copy the code

app/post.ts

import path from "path";
import fs from "fs/promises";
import parseFrontMatter from "front-matter";

export type Post = {
  slug: string;
  title: string;
};

// relative to the server output not the source!
let postsPath = path.join(__dirname, ".."."posts");

export async function getPosts() {
  let dir = await fs.readdir(postsPath);
  return Promise.all(
    dir.map(async filename => {
      let file = await fs.readFile(
        path.join(postsPath, filename)
      );
      let { attributes } = parseFrontMatter(
        file.toString()
      );
      return {
        slug: filename.replace(/\.md$/.""),
        title: attributes.title }; })); }Copy the code

This is not a Tutorial on the Node filesystem, so you just have to trust our code. As mentioned earlier, you can get markdown content from a database somewhere (we’ll show you in a later tutorial).

{% note Danger Simple %} If you are not using Remix App Server, you will need to add additional “..” to the path. , also note that you cannot deploy and demonstrate the environment in an environment without a persistent file system.

TypeScript is angry about that code, let’s make it happy.

Since we’re reading a file, but the type system doesn’t know what’s in it, we need a run-time check, and for that we need a invariant method to make the run-time check easy.

๐Ÿ’ฟ ensures that our articles have the right metadata and are type-safe

npm add tiny-invariant
Copy the code

app/post.ts

import path from "path";
import fs from "fs/promises";
import parseFrontMatter from "front-matter";
import invariant from "tiny-invariant";

export type Post = {
  slug: string;
  title: string;
};

export type PostMarkdownAttributes = {
  title: string;
};

let postsPath = path.join(__dirname, ".."."posts");

function isValidPostAttributes(
  attributes: any
) :attributes is PostMarkdownAttributes {
  returnattributes? .title; }export async function getPosts() {
  let dir = await fs.readdir(postsPath);
  return Promise.all(
    dir.map(async filename => {
      let file = await fs.readFile(
        path.join(postsPath, filename)
      );
      let { attributes } = parseFrontMatter(
        file.toString()
      );
      invariant(
        isValidPostAttributes(attributes),
        `${filename}has bad meta data! `
      );
      return {
        slug: filename.replace(/\.md$/.""),
        title: attributes.title }; })); }Copy the code

Even if you don’t use TypeScript, you’ll want to check on invariant so you know what’s wrong. Ok! Going back to the user interface, we should take a look at our list of posts. Feel free to add more posts, refresh and watch the list grow.

Dynamic routing parameters

Now let’s make a route to view the article. We want these urls to be valid:

/posts/my-first-post
/posts/90s-mix-cdr
Copy the code

Instead of creating a route for each article, we can use “dynamic parameters” in UR, and Remix will parse the parameters and pass them to us so we can dynamically find the article.

๐Ÿ’ฟ create a dynamic route “app/routes/posts/$slug.tsx”

touch app/routes/posts/\$slug.tsx
Copy the code

app/routes/posts/$slug.tsx

export default function PostSlug() {
  return (
    <div>
      <h1>Some Post</h1>
    </div>
  );
}
Copy the code

You can click on one of the articles and you will see a new page

๐Ÿ’ฟ add an access parameter to the loader app/routes/posts/$slug.tsx

import { useLoaderData } from "remix";

export let loader = async ({ params }) => {
  return params.slug;
};

export default function PostSlug() {
  let slug = useLoaderData();
  return (
    <div>
      <h1>Some Post: {slug}</h1>
    </div>
  );
}
Copy the code

What starts with $in the file path will become the loader’s argument, and that’s how we’ll find the article.

๐Ÿ’ฟ let’s get some help with loader functions in TypeScript

app/routes/posts/$slug.tsx

import { useLoaderData } from "remix";
import type { LoaderFunction } from "remix";

export let loader: LoaderFunction = async ({ params }) => {
  return params.slug;
};
Copy the code

Now let’s actually get the article information from the file system

๐Ÿ’ฟ adds a getPost function for our POST module

This function will be placed anywhere in the app/post.ts module:

// ...
export async function getPost(slug: string) {
  let filepath = path.join(postsPath, slug + ".md");
  let file = await fs.readFile(filepath);
  let { attributes } = parseFrontMatter(file.toString());
  invariant(
    isValidPostAttributes(attributes),
    `Post ${filepath} is missing attributes`
  );
  return { slug, title: attributes.title };
}
Copy the code

๐Ÿ’ฟ uses the new getPost app/routes/posts/$slug.tsx in the route

import { useLoaderData } from "remix";
import type { LoaderFunction } from "remix";
import { getPost } from "~/post";
import invariant from "tiny-invariant";

export let loader: LoaderFunction = async ({ params }) => {
  invariant(params.slug, "expected params.slug");
  return getPost(params.slug);
};

export default function PostSlug() {
  let post = useLoaderData();
  return (
    <div>
      <h1>{post.title}</h1>
    </div>
  );
}
Copy the code

Check it out! We’ll get our articles from the data source rather than include them all in the browser as JavaScript.

Just a quick note from Invariant, since params comes from the URL, we’re not entirely sure that params.slug will be defined, maybe you’ll change the file to $postid.ts! Invariant validates this is a good way to make TypeScript happy, too.

There are many markdown parsers, and we’ll use “marked” in this tutorial because it’s so easy to get started.

๐Ÿ’ฟ parses markdown to HTML

app/post.ts

import path from "path";
import fs from "fs/promises";
import parseFrontMatter from "front-matter";
import invariant from "tiny-invariant";
import { marked } from "marked";

/ /...
export async function getPost(slug: string) {
  let filepath = path.join(postsPath, slug + ".md");
  let file = await fs.readFile(filepath);
  let { attributes, body } = parseFrontMatter(
    file.toString()
  );
  invariant(
    isValidPostAttributes(attributes),
    `Post ${filepath} is missing attributes`
  );
  let html = marked(body);
  return { slug, html, title: attributes.title };
}
Copy the code

๐Ÿ’ฟ render HTML in route

app/routes/posts/$slug.tsx

// ...
export default function PostSlug() {
  let post = useLoaderData();
  return (
    <div dangerouslySetInnerHTML={{ __html: post.html}} / >
  );
}
Copy the code

Holy Smokes, you did it. You got a blog.

Create a Blog post

Now our blog post (with type fixes) is about deployment. Trouble, the idea here is that your posts will be supported by a database, so we need a new way to create blog posts. We’re going to do this in the future.

Let’s create a new “admin” section for the application

๐Ÿ’ฟ Create an admin route

touch app/routes/admin.tsx
Copy the code

app/routes/admin.tsx

import { Link, useLoaderData } from "remix";
import { getPosts } from "~/post";
import type { Post } from "~/post";

export let loader = () = > {
  return getPosts();
};

export default function Admin() {
  let posts = useLoaderData<Post[]>();
  return (
    <div className="admin">
      <nav>
        <h1>Admin</h1>
        <ul>
          {posts.map(post => (
            <li key={post.slug}>
              <Link to={post.slug}>{post.title}</Link>
            </li>
          ))}
        </ul>
      </nav>
      <main>.</main>
    </div>
  );
}
Copy the code

You should recognize a lot of code from the POST route. To set up some extra HTML, we need to set up some styles.

๐Ÿ’ฟ Create the admin stylesheet

touch app/styles/admin.css
Copy the code

app/styles/admin.css

.admin {
  display: flex;
}

.admin > nav {
  padding-right: 2rem;
}

.admin > main {
  flex: 1;
  border-left: solid 1px #ccc;
  padding-left: 2rem;
}

em {
  color: red;
}
Copy the code

๐Ÿ’ฟ links the stylesheet app/routes/admin.tsx in the admin route

import { Link, useLoaderData } from "remix";
import { getPosts } from "~/post";
import type { Post } from "~/post";
import adminStyles from "~/styles/admin.css";

export let links = () = > {
  return [{ rel: "stylesheet".href: adminStyles }];
};

// ...
Copy the code

Each route to can export a links method that returns an array of values as , in object form rather than HTML. We use {rel: “stylesheet”, href: adminStyles} instead of . This allows Remix to merge all rendered route Links together and render them in the element at the top of the document. You can see this at root.tsx if you’re curious.

Ok, now you should have a nice looking page with articles on the left and placeholders on the right.

The index routing

Let’s populate this placeholder with admin’s index. Along with us, we’re introducing “nesting” here, where your routing file nesting becomes UI component nesting.

๐Ÿ’ฟ creates an indexed folder for the subroutes of admin.tsx

mkdir app/routes/admin
touch app/routes/admin/index.tsx
Copy the code

app/routes/admin/index.tsx

import { Link } from "remix";

export default function AdminIndex() {
  return (
    <p>
      <Link to="new">Create a New Post</Link>
    </p>
  );
}
Copy the code

You won’t see it yet if you refresh, each route in app/routes/admin/ can be rendered in app/routes/admin.tsx when their URL matches. You can control which part of the admin.tsx layout the child routes are rendered in.

๐Ÿ’ฟ adds an outlet to the admin page

import { Outlet, Link, useLoaderData } from "remix";

`app/routes/admin.tsx`
/ /...
export default function Admin() {
  let posts = useLoaderData<Post[]>();
  return (
    <div className="admin">
      <nav>
        <h1>Admin</h1>
        <ul>
          {posts.map(post => (
            <li key={post.slug}>
              <Link to={post.slug}>{post.title}</Link>
            </li>
          ))}
        </ul>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}
Copy the code

Wait a minute, indexed routes can be confusing at first. As long as you know the path where the URL matches the parent route, the index will be presented in the outlet.

Maybe this will help, let’s add the “/admin/new” route and see what happens when we click the link.

๐Ÿ’ฟ Create app/routes/admin/new. TSX routes

touch app/routes/admin/new.tsx
Copy the code

app/routes/admin/new.tsx

export default function NewPost() {
  return <h2>New Post</h2>;
}
Copy the code

Now click on the link in the indexed route and watch that
is automatically replaced with a “new” route.

action

Now we’re going to get serious. Let’s build a form so that we can build a new article via the “new” route

๐Ÿ’ฟ add a form app/routes/admin/new.tsx for the new route

import { Form } from "remix";

export default function NewPost() {
  return (
    <Form method="post">
      <p>
        <label>
          Post Title: <input type="text" name="title" />
        </label>
      </p>
      <p>
        <label>
          Post Slug: <input type="text" name="slug" />
        </label>
      </p>
      <p>
        <label htmlFor="markdown">Markdown</label>
        <br />
        <textarea rows={20} name="markdown" />
      </p>
      <p>
        <button type="submit">Create Post</button>
      </p>
    </Form>
  );
}
Copy the code

If you like HTML as much as we do, you should be very excited. If you’ve done a lot of < Form onSubmit> and

For this kind of functionality, all you need is a form to get data from the user and a way to handle its back-end operations. In Remix, that’s all you need to do.

Let’s start by creating the basic code for how to save articles in the post.ts module.

๐Ÿ’ฟ add createPost to app/post.ts anywhere in app/post.ts

// ...
export async function createPost(post) {
  let md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`;
  await fs.writeFile(
    path.join(postsPath, post.slug + ".md"),
    md
  );
  return getPost(post.slug);
}
Copy the code

๐Ÿ’ฟ call createPost app/routes/admin/new.tsx from the new POST route

// ...
type NewPost = {
  title: string;
  slug: string;
  markdown: string;
};

export async function createPost(post: NewPost) {
  let md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`;
  await fs.writeFile(
    path.join(postsPath, post.slug + ".md"),
    md
  );
  return getPost(post.slug);
}

/ /...
Copy the code

That’s it, and Remix(and browser) will do the rest. Click the Submit button and view the new files that will be updated in the sidebar of our article page.

The name attribute entered in the HTML is sent over the network and retrieved by the same name in the formData request.

TypeScript is unhappy again, let’s add some types.

๐Ÿ’ฟ add the changes to both files

app/posts.ts

// ...
type NewPost = {
  title: string;
  slug: string;
  markdown: string;
};

export async function createPost(post: NewPost) {
  let md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`;
  await fs.writeFile(
    path.join(postsPath, post.slug + ".md"),
    md
  );
  return getPost(post.slug);
}

/ /...
Copy the code

app/routes/admin/new.tsx

import { Form, redirect } from "remix";
import type { ActionFunction } from "remix";
import { createPost } from "~/post";

export let action: ActionFunction = async ({ request }) => {
  let formData = await request.formData();

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

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

  return redirect("/admin");
};
Copy the code

Whether you use TypeScript or not, we run into problems when users don’t set some of these fields (and TS is still angry about calling createPost)

Let’s add some validation before creating the article.

๐Ÿ’ฟ verifies that the form data contains the data we need and returns an error if it does not

app/routes/admin/new.tsx

/ /...
export let action: ActionFunction = async ({ request }) => {
  let formData = await request.formData();

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

  let errors = {};
  if(! title) errors.title =true;
  if(! slug) errors.slug =true;
  if(! markdown) errors.markdown =true;

  if (Object.keys(errors).length) {
    return errors;
  }

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

  return redirect("/admin");
};
Copy the code

Notice that we’re not returning a redirect this time, we’re actually returning an error. These errors can be provided to the component through useActionData. Just like useLoaderData, but the data comes from the form’s actions after POST.

๐Ÿ’ฟ Add authentication message app/routes/admin/new.tsx to UI

import { useActionData, Form, redirect } from "remix";

// ...

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

  return (
    <Form method="post">
      <p>
        <label>Post Title:{" "} {errors? .title &&<em>Title is required</em>}
          <input type="text" name="title" />
        </label>
      </p>
      <p>
        <label>Post Slug:{" "} {errors? .slug &&<em>Slug is required</em>}
          <input type="text" name="slug" />
        </label>
      </p>
      <p>
        <label htmlFor="markdown">Markdown:</label>{" "} {errors? .markdown &&<em>Markdown is required</em>}
        <br />
        <textarea rows={20} name="markdown" />
      </p>
      <p>
        <button type="submit">Create Post</button>
      </p>
    </Form>
  );
}
Copy the code

TypeScript is still not happy so let’s add some invariants to make it happy. app/routes/admin/new.tsx

/ /...
import invariant from "tiny-invariant";

export let action: ActionFunction = async ({ request }) => {
  // ...

  if (Object.keys(errors).length) {
    return errors;
  }

  invariant(typeof title === "string");
  invariant(typeof slug === "string");
  invariant(typeof markdown === "string");
  await createPost({ title, slug, markdown });

  return redirect("/admin");
};
Copy the code

For fun, try again after disabling JavaScript in your development tools. Because Remix is built on top of Http and HTML, there is no need to use JavaScript in the browser. That’s not the point, let’s slow down and add some “agent UI” to our form.

app/routes/admin/new.tsx

// ...
export let action: ActionFunction = async ({ request }) => {
  await new Promise(res= > setTimeout(res, 1000));
  let formData = await request.formData();
  let title = formData.get("title");
  let slug = formData.get("slug");
  let markdown = formData.get("markdown");
  // ...
};
/ /...
Copy the code

๐Ÿ’ฟ Use useTransition to add some to-do UI app/routes/admin/new.tsx

import {
  useTransition,
  useActionData,
  Form,
  redirect
} from "remix";

// ...

export default function NewPost() {
  let errors = useActionData();
  let transition = useTransition();

  return (
    <Form method="post">{/ *... * /}<p>
        <button type="submit">
          {transition.submission
            ? "Creating..."
            : "Create Post"}
        </button>
      </p>
    </Form>
  );
}
Copy the code

Users now have a much better experience than when we did this with no JavaScript at all in the browser. Other things you can do like insert the title automatically into the SLug field, or have the user overwrite it (maybe we’ll add that later)

That’s all for today! Your assignment is to make a /admin/ Edit page for your article. These links are already in the sidebar, but they are 404! Create a new route to read articles and place them in fields. All the code you need is already in app/routes/posts/$slug.ts and app/routes/posts/new.ts. You just have to put it together.

We hope you enjoy Remix.


  1. If you get this step wrong, check out Super Simple Start to Remix at โ†ฉ
  2. The original text is used herehydratedThe literal translation should beThe hydration.โ†ฉ