Remix
What is want
Remix is a full-stack Web framework that lets you focus on the user interface and relearn the basics of the Web to provide users with a fast and smooth user experience.
React Router is a React based framework developed by the React Router team. Compared to other frameworks, this framework focuses on the concept of “nested routines”, allowing components to connect directly to other pages, simplifying code writing.
Use the official Remix documentation to build the Jokes application to see what Remix has to offer.
Front knowledge
- TypeScript, this tutorial will be written in TypeScript. If you’re not familiar with TypeScript, check out the official TypeScript manual. You can also write in JavaScript.
- React Remix is a full-stack framework based on React. You may need to understand some of the basics of React. The React documentation has been updated to include hooks.
The development environment
Node >= 14
npm >= 7
You can check your Node environment. I'm using 16.13.1
node -v
# v16.13.1
npm -v
# 8.1.2
You can use NVM/window-nvm to install the corresponding node versionNVM install v16.13.1Or install stable directly
nvm install stable
Then use the newly installed version of NodeNVM use v16.13.1# nvm use stable
Copy the code
Create the Remix project
1. Open your terminal and run the following command
npx create-remix@latest
Copy the code
After executing, you may be asked if you want to install create-remix to execute the command. Press Y to confirm that it will only be temporarily installed to execute the setup script
When the installation is complete, it will ask you some questions
R E M I X
💿 Welcome to Remix! Let's get you set up with a new project.
? Where would you like to create your app? remix-jokes
? Where do you want to deploy? Choose Remix if you're unsure, it's easy to change deployment targets. Remix
App Server
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes
Copy the code
The Remix App Server option is an Express-based Node.js Server.
When NPM install is complete, we CD remix-jokes into the directory and open code through the first IDE in the universe –VSCode.
The directory structure
Re-mix-bass Exercises ── ─ Bass Exercises. Md exercises ── app │ ├─ Enough.custom.tsX │ ├── Root.tsX │ ├─ Routes │ ├─ Demos │ │ │ ├ ─ ─ the about │ │ │ │ ├ ─ ─ index. The TSX │ │ │ │ └ ─ ─ whoa. The TSX │ │ │ ├ ─ ─ the about the TSX │ │ │ ├ ─ ─ actions. The TSX │ │ │ ├ ─ ─ Correct. The TSX │ │ │ ├ ─ ─ params │ │ │ │ ├ ─ ─ $id. The TSX │ │ │ │ └ ─ ─ index. The TSX │ │ │ └ ─ ─ params. The TSX │ │ └ ─ ─ index. The TSX │ └ ─ ─ Styles │ ├─ dark.css │ ├─ demos │ ├─ about.css │ ├─ global │ ├─ package.json │ ├─ public │ ├.css │ ├─ download.css │ ├─ download.json │ ├─ public │ ├.css │ ├─ download.css │ ├─ download.json │ ├─ public │ ├.css │ ├─ download.css │ ├─ download.json │ ├─ public │ ├─ └─ favicon.ico ├─ remix.env.js ├─ remix.env.txt TXT TXT TXT TXT TXT TXT TXT TXTCopy the code
app/
This is where your Remix app code is stored, likevue-cli
/create-react-app
Build the project insidesrc/
app/entry.client.tsx
Remix is used to run the JavaScript code in this file when the application is finished loading in the browserhydrateThe React components. What does hydrate mean? The React component is initialized by the browser based on the string returned by the server during rendering. This process is called “waterflood”.app/entry.server.tsx
The JavaScript code in this file runs the first time a request is sent to the server. Remix will load all the necessary data for you, and we will take care of sending the response back to the browser.app/root.tsx
This file is the root component of the application, just like Vue’sApp.vue
, the web pagehtml
This is where the tag will be rendered.app/routes/
This path will store all module files related to routing, and Remix will create the corresponding URL route based on the file name under this directory.public/
This is where static resources such as images, fonts, etc. are stored in your project as usual.remix.config.js
The Remix application configuration is written in this file.
Let’s try packing up the files
npm run build
Copy the code
After the packaging is complete, the following folders appear under the current directory
.cache/
The contents of this folder are for use within Remixbuild/
Is server-side codepublic/build/
Is the client code
Add these paths to the.gitignore file
/.cache
/build
/public/build
Copy the code
You want to run the packaged application execution
npm start
Copy the code
Open the link and you will see the following web interface
Click on the link if you want to check out the Remix Starter, but this will be covered in the rest of the tutorial.
Ctrl + C to stop the service, then delete the folder below
app/routes
app/styles
Then replace the code inside the app/root. TSX with the ones inside the “Jokes app”, I won’t copy them, readers can copy and paste them into their own projects.
The key code is this section {process.env.node_env === “development”?
Now your app/ directory is structured like this
├── Enact.client. TSX ├─ enact.server. TSX ├─ root.tsxCopy the code
Then run NPM run Dev to see that the entire page is different from the original Remix Starter.
routing
Remix routes can be defined in two ways
-
Create a route using remix.config.js
-
Another, more common method in Remix is to create routes in the routes/ folder
remix.config.js
module.exports = {
appDirectory: "app".// For the app folder, you can also change it to SRC
browserBuildDirectory: "public/build".// Where the client code is stored after packaging
devServerPort: 8002.publicPath: "/build/".// Static resource storage path
serverBuildDirectory: "build".// The path where the server code is stored after packaging
routes(defineRoutes) {
return defineRoute(route= > {
// The first argument is the React Router path to match
// The second parameter is the corresponding file to process
route("/somewhere/cool/*"."catchall.tsx");
// If you are nested, pass an optional callback method in the third argument
route("some/:path"."some/route/file.js".() = > {
// - Path is relative to parent path
// - The file name is still relative to the app path and not the parent path
route("relative/path"."some/other/file")}); }); }// The following is an example of my work
async routes(defineRoutes) {
return defineRoutes((route) = > {
route("test/:path"."routes/test/index.tsx".() = > {
route("relative/child1"."routes/test/child.tsx");
route("relative/child2"."routes/test/child2.tsx"); }); }); }};Copy the code
Create the test folder under routes/
├── Child. ├─ child. ├─ childCopy the code
// index.tsx
import { Outlet } from "remix";
export default function TestIndexRoute() {
return (
<div>
Test remix.config.js define nested routes <br />
<Outlet />
</div>
);
}
// child.tsx
export default function ChildRoute() {
return <div>Child1</div>;
}
// child2.tsx
export default function ChildRoute() {
return <div>Child2</div>;
}
Copy the code
http://localhost:3000/test/ plus any path will match to the test/index. The TSX this file
http://localhost:3000/test/1234/relative/chlid1 will match to the test/child. The TSX this file
The other is the file-based routing demonstrated in the official tutorial
Put the files in the app/routes/ folder. These files are called “routing modules” and should follow Remix’s file routing naming convention
Routing file naming
-
App/root. The TSX root routing
-
App /routes/*.{js,ts, JSX, TSX,md, MDX} : Any file in this folder will become the application route. Remix naturally supports these files
-
App /routes/{folder}/*.tsx: Files in the folder will create nested routines
-
App /routes/{folder} and app/routes/{folder}. TSX When there is a file in app/routes/ folder with the same name as the folder name, the file will be used as a layout for the file routing in the folder. Render a
inside {Folder}.tsx, and the child path will render instead of the
component. I’ll show you later -
Adding a. To a file name: Adding a. To a file name creates a nested path, but its layout will not be rendered above
in {folder.tsx}. For example, the official tutorial’s jokes. Funny. TSX creates a URL path to /jokes/funny that is a child of /jokes. -
Index.tsx: Displays by default when the parent path matches exactly.
-
$param: represents the dynamic part of the URL that Remix parses and passes to ‘loader’ functions and routes. For example, ‘app/routes/users/’ represents the dynamic part of the URL. Remix will help us parse and pass it to the ‘loader’ function and route. For example, ‘app/routes/users/’ represents the dynamic part of the URL. Remix will help us parse and pass it to the ‘loader’ function and route. Such as the ‘app/routes/users/userId TSX when matching to the path of the browser to/users / 1234, the export const loader: LoaderFunction = async ({params}) => Params. userId === ‘1234’ ‘in {}
-
App /routes/files/$. TSX: capture all files/* path, let’s write a simple example to explain
// routes/test.tsx export default function TestRoute() { return <div>test</div>; } // routes/test.$.tsx import { LoaderFunction } from "remix"; export const loader: LoaderFunction = async ({ params }) => { console.log(params); return null; }; export default function Test$Route() { return <div>catch rest route</div>; } // Route ('test/:path') in remix.config.js needs to be modified otherwise all paths will be matched async routes(defineRoutes) { return defineRoutes((route) = > { // route("test/:path", "routes/test/index.tsx", () => { route("test/haha"."routes/test/index.tsx".() = > { route("relative/child1"."routes/test/child.tsx"); route("relative/child2"."routes/test/child2.tsx"); }); }); }, // test/haha Jumps to routes/test/index.tsx $. TSX Loader params === {'*': 'haha1'} $. TSX params === {'*': 'haha1/123'} // test/haha1/123? $. TSX params === {'*': 'haha1/123'} Copy the code
-
app/routes/__some-layout/some-path.tsx : Folder names prefixed with __ will create a “Layout Route”. This Layout Route is false and will not match your URL with /some-layout or /__some-layout. It will match if your path is /some-path, and some-latout will be shown as the parent route in its
.
├ ─ ─ app │ ├ ─ ─ routes │ │ ├ ─ ─ __layout │ │ │ ├ ─ ─ lmao. The TSX │ │ ├ ─ ─ __layout. The TSXCopy the code
// __layout.tsx
import { Outlet } from "remix";
export default function LayoutRoute() {
return (
<div>
__Layout <br />
<Outlet />
</div>
);
}
// lmao.tsx
export default function LmaoRoute() {
return <div>LMAO</div>;
}
// localhost:3000/__layout will be 404
Copy the code
style
Usually in order to make our web pages more beautiful, we need to write some CSS to beautify our website. We add tags like to load our CSS files, and Remix loads our CSS files in the same way. But Remix also supports CSS nesting, meaning that the CSS is loaded only when the current subroute is active, and the tag is removed when the user leaves the current page or jumps to a different route.
We can export the links function in the routing file, copy and paste the code from the official website
import type { LinksFunction } from "remix";
import stylesUrl from ".. /styles/index.css";
// The key code is this part, to export links
export const links: LinksFunction = () = > {
return [{ rel: "stylesheet".href: stylesUrl }];
};
Copy the code
But now you go to http://localhost:3000 and you see that the style doesn’t work. Since root. TSX is the root of everything rendered, including the < HTML >
, the
tag for loading CSS files needs to be added ourselves, and the Remix has already wrapped the
When you switch to another path, you will notice that the CSS style of the index page has disappeared. If you press F12 to open the Element TAB on the console, you will notice that the tag inside the TAB has been removed.
This means that you don’t have to worry about CSS collisions when writing CSS, and it also means that your CSS can be permanently cached and naturally code isolated
Global-large. CSS and global-media. CSS. If you click on the global-large. CSS file, you will find that the file does not use the syntax of media query.
export const links: LinksFunction = () = > {
return[{rel: "stylesheet".href: globalStylesUrl,
},
{
rel: "stylesheet".href: globalMediumStylesUrl,
media: "print, (min-width: 640px)"}, {rel: "stylesheet".href: globalLargeStylesUrl,
media: "screen and (min-width: 1024px)",},]; };Copy the code
In fact, many people should not use the tag media query, MDN can look up the related attributes.
LinksFunction support two types HtmlLinkDescriptor | PageLinkDescriptor
An HtmlLinkDescriptor is a tag in the form of an object. For details, see the MDN link above
LinksFunction of the type PageLinkDescriptor allows you to pre-load JavaScript modules, loader data and styles into the browser’s cache at any time the user might be able to access a path, notice it’s possible.
The database
To actually develop a project, we usually need to store data. In the tutorial on the website, we will use our own SQLite database rather than some third-party persistence solution.
Set the Prisma
If you are using VSCode, you can install their plugin by searching for prisma in the plugin market. This will prompt you to write prisma syntax later, which is very convenient. Prisma is an Object Relational Mapping database for those who are not familiar with databases.
Now we need to install two packages so we can continue with the tutorial
npm install --save-dev prisma
npm install @prisma/client
Copy the code
And then initialize it
npx prisma init --datasource-provider sqlite
Copy the code
The terminal will then print the following text and we have initialized successfully
✔ Prisma schema was created at Prisma /schema. Prisma You can now open it in Your favorite editor. Warn You already have a .gitignore. Don't forget to exclude .env to not commit any secret. Next steps: 1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started 2. Run prisma db pull to turn your database schema into a Prisma schema. 3. Run prisma generate to generate the Prisma Client. You can then start querying your database. More information in our documentation: https://pris.ly/d/getting-startedCopy the code
Copy and paste the tutorial code and run it
npx prisma db push
Copy the code
Our database file will be created prisma /dev/db, and importantly it will help us generate TypeScript types that will be prompted when we call the PRISma API during development.
Add prisma/dev. Db and the.env printed above to the.gitignore file.
If the database is cracked or broken, delete dev.db and run NPX prisma DB push
Then we need to add some to the database and create a prisma/seed.ts file
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
async function seed() {
await Promise.all(
getJokes().map(joke= > {
return db.joke.create({ data: joke }); })); } seed();function getJokes() {
// shout-out to https://icanhazdadjoke.com/
return[{name: "Road worker".content: `I never wanted to believe that my Dad was stealing from his job as a road worker. But when I got home, all the signs were there.`
},
{
name: "Frisbee".content: `I was wondering why the frisbee was getting bigger, then it hit me.`
},
{
name: "Trees".content: `Why do trees seem suspicious on sunny days? Dunno, they're just a bit shady.`
},
{
name: "Skeletons".content: `Why don't skeletons ride roller coasters? They don't have the stomach for it.`
},
{
name: "Hippos".content: `Why don't you find hippopotamuses hiding in trees? They're really good at it.`
},
{
name: "Dinner".content: `What did one plate say to the other plate? Dinner is on me! `
},
{
name: "Elevator".content: `My first time using an elevator was an uplifting experience. The second time let me down.`}]; }Copy the code
Install esbuilder – register
npm install --save-dev esbuild-register
Copy the code
Then run seed.ts
node --require esbuild-register prisma/seed.ts
Copy the code
Now our database will have the data written in the above file, run the following command to see the corresponding table and data
npx prisma studio
Copy the code
But if you run out of data after resetting the database, you can add commands to package.json so that you don’t have to execute them every time.
// ...
"prisma": {
"seed": "node --require esbuild-register prisma/seed.ts"
},
"scripts": {
// ...
Copy the code
Connecting to a Database
If you follow the steps above to link the database
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
Copy the code
It does connect to the database, but during our development, @remix-run/serve repackaged it for us, so that every time we updated our code, we created a new link to the database, and Prisma eventually issued a Warning: 10 Prisma Clients are already running. To solve this problem we will create a new file called app/utils/db.server.ts
import { PrismaClient } from "@prisma/client";
let db: PrismaClient;
declare global {
var __db: PrismaClient | undefined;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
if (process.env.NODE_ENV === "production") {
db = new PrismaClient();
db.$connect();
} else {
if (!global.__db) {
global.__db = new PrismaClient();
global.__db.$connect();
}
db = global.__db;
}
export { db };
Copy the code
There is a specification laid down by Remix that files with.server are ultimately not packaged into client code.
Read database data in loader
In Remix, each routing module can obtain data by exporting a Loader function, which is called on the server side.
params
The route parameters are passed to the Loader function
// if the user visits /invoices/123
export let loader: LoaderFunction = ({ params }) = > {
params.invoiceId; / / "123"
};
Copy the code
request
This parameter is the Fetch Request instance with the Request information, refer to the attribute description in the MDN documentation. This parameter is usually used to read the contents of headers or the current URL path
export let loader: LoaderFunction = ({ request }) = > {
// read a cookie
let cookie = request.headers.get("Cookie");
// parse the search params
let url = new URL(request.url);
let search = url.searchParams.get("search");
};
Copy the code
context
This context property is available in the server adapter’s getLoadContext() function, which fills the gap between the adapter’s Request/Response API (a backdoor property that is generally not needed).
const {
createRequestHandler
} = require("@remix-run/express");
app.all(
"*",
createRequestHandler({
getLoadContext(req, res) {
// this becomes the loader context
return { expressUser: req.user }; }}));Copy the code
Then you can get it from loader
export let loader: LoaderFunction = ({ context }) = > {
let { expressUser } = context;
// ...
};
Copy the code
In the example that gets jokes on the official website, a loader function is used to fetch the data from the database and return it as an object. UseLoaderData () is used to fetch the data and render it to the page.
The data submitted
The example on the official website uses a native HTML form. I believe many people use Vue or React as a front-end framework for development. There are many open source UI libraries and axios as a wrapped request library. Very few people will ever use forms as a more primitive form of data submission. But in Remix, instead of installing some fancy UI libraries and Axios, we just export an action function.
An action, like the loader mentioned above, is a method called on the server to process data passed by the browser or do other operations. This action function will be invoked before the Loader if it is not a GET request.
Actions have a similar API to Loaders, except that they are called at different times.
import type { ActionFunction } from "remix";
import { redirect } from "remix";
import { db } from "~/utils/db.server";
export const action: ActionFunction = async ({
request
}) => {
const form = await request.formData();
const name = form.get("name");
const content = form.get("content");
// we do this type check to be extra sure and to make TypeScript happy
// we'll explore validation next!
if (
typeofname ! = ="string" ||
typeofcontent ! = ="string"
) {
throw new Error(`Form not submitted correctly.`);
}
const fields = { name, content };
const joke = await db.joke.create({ data: fields });
return redirect(`/jokes/${joke.id}`);
};
export default function NewJokeRoute() {
return (
<div>
<p>Add your own hilarious joke</p>
<form method="post">
<div>
<label>
Name: <input type="text" name="name" />
</label>
</div>
<div>
<label>
Content: <textarea name="content" />
</label>
</div>
<div>
<button type="submit" className="button">
Add
</button>
</div>
</form>
</div>
);
}
Copy the code
Get (“name”) is a native Web API, and that’s one of the great things about Remix. As you learn more about the Web, instead of learning a new framework, Then many things in my framework can only be limited to this set of ecology, learning costs are not so big.
The redirect function is a simple tool provided by Remix to create a Response with the correct return code and request status and redirect it to the user
The return value of an action, like the loader, can accept either a serializable object or a Response, as in the validation form below.
import type { ActionFunction } from "remix";
import { useActionData, redirect, json } from "remix";
import { db } from "~/utils/db.server";
function validateJokeContent(content: string) {
if (content.length < 10) {
return `That joke is too short`; }}function validateJokeName(name: string) {
if (name.length < 2) {
return `That joke's name is too short`; }}typeActionData = { formError? :string; fieldErrors? : {name: string | undefined;
content: string | undefined; }; fields? : {name: string;
content: string;
};
};
const badRequest = (data: ActionData) = >
json(data, { status: 400 });
export const action: ActionFunction = async ({
request
}) => {
const form = await request.formData();
const name = form.get("name");
const content = form.get("content");
if (
typeofname ! = ="string" ||
typeofcontent ! = ="string"
) {
return badRequest({
formError: `Form not submitted correctly.`
});
}
const fieldErrors = {
name: validateJokeName(name),
content: validateJokeContent(content)
};
const fields = { name, content };
if (Object.values(fieldErrors).some(Boolean)) {
return badRequest({ fieldErrors, fields });
}
const joke = await db.joke.create({ data: fields });
return redirect(`/jokes/${joke.id}`);
};
export default function NewJokeRoute() {
const actionData = useActionData<ActionData>();
return (
<div>
<p>Add your own hilarious joke</p>
<form method="post">
<div>
<label>
Name:{" "}
<input
type="text"
defaultValue={actionData? .fields? .name}
name="name"
aria-invalid={
Boolean(actionData?.fieldErrors?.name) | |undefined
}
aria-describedby={
actionData?.fieldErrors?.name
? "name-error"
: undefined} / >
</label>{actionData? .fieldErrors? .name ? (<p
className="form-validation-error"
role="alert"
id="name-error"
>
{actionData.fieldErrors.name}
</p>
) : null}
</div>
<div>
<label>
Content:{" "}
<textarea
defaultValue={actionData? .fields? .content}
name="content"
aria-invalid={
Boolean(actionData?.fieldErrors?.content) | |undefined
}
aria-describedby={
actionData?.fieldErrors?.content
? "content-error"
: undefined} / >
</label>{actionData? .fieldErrors? .content ? (<p
className="form-validation-error"
role="alert"
id="content-error"
>
{actionData.fieldErrors.content}
</p>
) : null}
</div>
<div>
<button type="submit" className="button">
Add
</button>
</div>
</form>
</div>
);
}
Copy the code
permissions
NPM install — save-dev@types /bcrypt
The login form
import type { LinksFunction } from "remix";
import { Link, useSearchParams } from "remix";
import stylesUrl from ".. /styles/login.css";
export const links: LinksFunction = () = > {
return [{ rel: "stylesheet".href: stylesUrl }];
};
export default function Login() {
const [searchParams] = useSearchParams();
return (
<div className="container">
<div className="content" data-light="">
<h1>Login</h1>
<form method="post">
<input
type="hidden"
name="redirectTo"
value={
searchParams.get("redirectTo")??undefined} / >
<fieldset>
<legend className="sr-only">
Login or Register?
</legend>
<label>
<input
type="radio"
name="loginType"
value="login"
defaultChecked
/>{" "}
Login
</label>
<label>
<input
type="radio"
name="loginType"
value="register"
/>{" "}
Register
</label>
</fieldset>
<div>
<label htmlFor="username-input">Username</label>
<input
type="text"
id="username-input"
name="username"
/>
</div>
<div>
<label htmlFor="password-input">Password</label>
<input
id="password-input"
name="password"
type="password"
/>
</div>
<button type="submit" className="button">
Submit
</button>
</form>
</div>
<div className="links">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/jokes">Jokes</Link>
</li>
</ul>
</div>
</div>
);
}
Copy the code
In actual service scenarios, some functions can only be used after the user logs in. When the user wants to use these functions, the user will be redirected to the login page. After the user logs in, the user will be redirected to the previous page instead of the home page. In the example on the official website, use useSearchParams to get the redirectTo parameter from the URL and place it inside the
// example http://localhost:3000/login? redirectTo=%2Fjokes%2Fnew console.log(useSearchParams()) [ URLSearchParams { 'redirectTo' => '/jokes/new' }, [Function (anonymous)] ]Copy the code
After we log in with the kody account, the terminal will print the following information, which means that our login is successful.
User: {id: '161CC267-DA50-459b-b6DD-3BFD68820DBC ', createdAt:' 2021-12-05T04:13:26.242z ', updatedAt: 'the 2021-12-05 T04: ". 242 z, the username:' kody, passwordHash: '$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu/1u' }Copy the code
Website using the Remix is bringing createCookieSessionStorage to store, specific use can be the reference document
If you want to add SESSION_SECRET=”my_secret” to the.env file, feel free
After login, redirect through the redirect function and set cookies in the second parameter of the function
export async function createUserSession(
userId: string,
redirectTo: string
) {
const session = await storage.getSession();
session.set("userId", userId);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await storage.commitSession(session)
}
});
}
Copy the code
We will then include RJ_session with each subsequent request.
Logout logic
There must be a login exit login, logout need to clear session, use destroySession this method, specific code can see the official website code
Remix will call loader if you log out of action, but will not fire loader if it is a link.
Error handling
Unexpected errors
Once the project is in production, there will be some unexpected problems, such as network problems, server crashes, and // @ts-ignore ignoring TypeScript warnings.
Error handling is one of the highlights of Remix. Similar to the Error Boundary function of React, Remix allows us to export an Error Boundary component, which can process loader and action data.
If you export the Error Boundary component directly from root.tsx you will find that the styles are gone, why is this? If you click on the Network option, you will see that the page sent back by the request looks like this
<! DOCTYPEhtml><div class="error-container"><h1>Oops! Something went wrong</h1><p>lol is not defined</p></div>
Copy the code
This is because root.tsx needs to render its own HTML tag and the corresponding tag, so we need to pull the original code out of a component
Use the Error Boundary component as a child so that the style will work.
The code below is the HTML returned, and you can see the , and websocket that supports hot update setup in the development environment
<! DOCTYPEhtml><html lang="en"><head><meta charSet="utf-8"/><title>Remix: So great, it's funny!</title><link rel="stylesheet" href="/build/_assets/global-3NTXPLP2.css"/><link rel="stylesheet" href="/build/_assets/global-medium-DRHJR3JT.css" media="print, (min-width: 640px)"/><link rel="stylesheet" href="/build/_assets/global-large-NKTQAWDZ.css" media="screen and (min-width: 1024px)"/><link rel="stylesheet" href="/build/_assets/jokes-MGLBGUHK.css"/></head><body><div class="error-container"><h1>Oops! Something went wrong</h1><p>lol is not defined</p></div><script>
let ws = new WebSocket("ws://localhost:8002/socket");
ws.onmessage = message= > {
let event = JSON.parse(message.data);
if (event.type === "LOG") {
console.log(event.message);
}
if (event.type === "RELOAD") {
console.log("💿 Reloading window...");
window.location.reload(); }}; ws.onerror =error= > {
console.log("Remix dev asset server web socket error:");
console.error(error);
};
</script></body></html>
Copy the code
Usually, the page will crash when there are some weird mistakes, or the whole page will show corresponding mistakes. The great thing about Remix is that it is nested, but $jokeid. TSX cannot be used, and its parent page can still be clicked to jump to the corresponding joke. This will make the user experience better without having to refresh or rewind the page.
useMatches
UseMatches can also be used in the Error Boundary component to get matching routes and data, throwing an Error in the loader of $jokeid.tsx
export const loader: LoaderFunction = async ({ request, params }) => {
const userId = await getUserId(request);
throw new Error("whoops");
console.log(params); // <-- {jokeId: "123"}
const joke = await db.joke.findUnique({ where: { id: params.jokeId } });
// if (! joke) throw new Error("Joke not found");
if(! joke) {throw new Response("What a joke! Not found.", {
status: 404}); }const data: LoaderData = {
joke,
isOwner: userId === joke.jokesterId,
};
return data;
};
// Use useMatches in ErrorBoundary
export function ErrorBoundary({ error }: { error: Error }) {
const params = useParams();
console.log(useMatches());
// throw ahha;
return (
<div className="error-container">{`Something went wrong when loading ${params.jokeId}!!! `}</div>
);
}
Copy the code
The printout looks like this
[{pathname: '/'.params: { jokeId: '88f54075-151a-48ed-9bb2-3b1615f5e53d' },
data: null.handle: undefined
},
{
pathname: '/jokes'.params: { jokeId: '88f54075-151a-48ed-9bb2-3b1615f5e53d' },
data: { jokes: [Array].user: null },
handle: undefined
},
{
pathname: '/jokes/88f54075-151a-48ed-9bb2-3b1615f5e53d'.params: { jokeId: '88f54075-151a-48ed-9bb2-3b1615f5e53d' },
data: null.handle: undefined}]Copy the code
Deal with unexpected errors
There are some errors that we know can happen, like >400 && < 500 for client and >500 for server.
For errors on the client side, Remix provides something similar to an Error Boundary called Catch Boundaries. Similar to loader and Action, useCatch obtains the thrown Response object, and then performs related processing according to the Response.
export const loader: LoaderFunction = async ({ request }) => {
const userId = await getUserId(request);
if(! userId) {throw new Response("Unauthorized", { status: 401 });
}
return {};
};
// ...
export function CatchBoundary() {
const caught = useCatch();
if (caught.status === 401) {
return (
<div className="error-container">
<p>You must be logged in to create a joke.</p>
<Link to="/login">Login</Link>
</div>); }}Copy the code
In
<form method="post">
<input type="hidden" name="_method" value="delete" />
<button type="submit">Delete</button>
</form>
Copy the code
This makes the request known in the action via request.formData().get(‘_method’) and the DELETE request
SEO
The most popular frame-packaged apps are single-page apps, which are notoriously SEO unfriendly. Remix provides a MetaFunction for us, we export a meta function, we can read the loader data inside and dynamically assign to the
tag. We will also import the
component from remix into the tag just as we loaded the CSS styles above.
import type { LinksFunction, MetaFunction } from "remix";
import {
Links,
LiveReload,
Outlet,
useCatch,
Meta
} from "remix";
import globalStylesUrl from "./styles/global.css";
import globalMediumStylesUrl from "./styles/global-medium.css";
import globalLargeStylesUrl from "./styles/global-large.css";
export const links: LinksFunction = () = > {
return[{rel: "stylesheet".href: globalStylesUrl
},
{
rel: "stylesheet".href: globalMediumStylesUrl,
media: "print, (min-width: 640px)"
},
{
rel: "stylesheet".href: globalLargeStylesUrl,
media: "screen and (min-width: 1024px)"}]; };export const meta: MetaFunction = () = > {
const description = `Learn Remix and laugh at the same time! `;
return {
description,
keywords: "Remix,jokes"."twitter:image": "https://remix-jokes.lol/social.png"."twitter:card": "summary_large_image"."twitter:creator": "@remix_run"."twitter:site": "@remix_run"."twitter:title": "Remix Jokes"."twitter:description": description
};
};
function Document({
children,
title = `Remix: So great, it's funny! `}: { children: React.ReactNode; title? :string;
}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<Meta />
<title>{title}</title>
<Links />
</head>
<body>
{children}
{process.env.NODE_ENV === "development" ? (
<LiveReload />
) : null}
</body>
</html>
);
}
export default function App() {
return (
<Document>
<Outlet />
</Document>
);
}
export function CatchBoundary() {
const caught = useCatch();
return (
<Document
title={` ${caught.status} ${caught.statusText} `} >
<div className="error-container">
<h1>
{caught.status} {caught.statusText}
</h1>
</div>
</Document>
);
}
export function ErrorBoundary({ error }: { error: Error }) {
return (
<Document title="Uh-oh!">
<div className="error-container">
<h1>App Error</h1>
<pre>{error.message}</pre>
</div>
</Document>
);
}
Copy the code
Resource routing
Sometimes we want our routes to render more than JUST HTML text, like PDFS, RSS feeds, etc. Use close parentheses to the. File names. Examples of RSS on the official website are clams [.]rss.tsx
import type { LoaderFunction } from "remix";
import { db } from "~/utils/db.server";
function escapeCdata(s: string) {
return s.replaceAll("]] >"."]]]] >
");
}
function escapeHtml(s: string) {
return s
.replaceAll("&"."&")
.replaceAll("<"."<")
.replaceAll(">".">")
.replaceAll('"'.""")
.replaceAll("'"."The & # 039;");
}
export const loader: LoaderFunction = async ({
request
}) => {
const jokes = await db.joke.findMany({
take: 100.orderBy: { createdAt: "desc" },
include: { jokester: { select: { username: true}}}});const host =
request.headers.get("X-Forwarded-Host")?? request.headers.get("host");
if(! host) {throw new Error("Could not determine domain URL.");
}
const protocol = host.includes("localhost")?"http"
: "https";
const domain = `${protocol}: / /${host}`;
const jokesUrl = `${domain}/jokes`;
const rssString = `
<rss xmlns:blogChannel="${jokesUrl}" version="2.0">
<channel>
<title>Remix Jokes</title>
<link>${jokesUrl}</link>
<description>Some funny jokes</description>
<language>en-us</language>
<generator>Kody the Koala</generator>
<ttl>40</ttl>
${jokes
.map(joke =>
` <item> <title><! [CDATA[${escapeCdata( joke.name )}]]></title> <description><! [CDATA[A funny joke called${escapeHtml( joke.name )}]]></description> <author><! [CDATA[${escapeCdata( joke.jokester.username )}]]></author>
<pubDate>${joke.createdAt.toUTCString()}</pubDate>
<link>${jokesUrl}/${joke.id}</link>
<guid>${jokesUrl}/${joke.id}</guid>
</item>
`.trim()
)
.join("\n")}
</channel>
</rss>
`.trim();
return new Response(rssString, {
headers: {
"Cache-Control": `public, max-age=The ${60 * 10
}, s-maxage=The ${60 * 60 * 24}`."Content-Type": "application/xml"."Content-Length": String(Buffer.byteLength(rssString))
}
});
};
Copy the code
Json “lib”: [“DOM”, “dom.iterable “, “ES2019”], “lib”: [“DOM”, “ES2019”] [“DOM”, “DOM.Iterable”, “ES2021”],
JavaScript
In previous development, those of you who are careful may have noticed that our application does not load JS files when our page refresh loads,
You can see that only the JS files for my Google plugin are loaded here, and there are no js files related to Remix. This is also one of the highlights of Remix!! Some might say, in this day and age, it wouldn’t hurt to load JavaScript. Is that an advantage? But after all, everyone’s situation is different, the network speed may be fast or slow, and the slow may need to wait for your application to load JavaScript before using the corresponding function, but if users can use the normal function without loading JS files, it will be a great improvement for the user experience.
However, it also has disadvantages. When we click the joke on the left to get the corresponding data, you will find that our web page will be refreshed. Although the hot update speed is very fast, it still refreshed the page.
In addition to the above, there are cases where JS files are required to run, such as UI presentations that require JS to determine when to display what UI.
We can import the
import {
Links,
LiveReload,
Outlet,
useCatch,
Meta,
Scripts
} from "remix";
// ...
function Document({
children,
title = `Remix: So great, it's funny! `}: { children: React.ReactNode; title? :string;
}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<Meta />
<title>{title}</title>
<Links />
</head>
<body>
{children}
<Scripts />
{process.env.NODE_ENV === "development" ? (
<LiveReload />
) : null}
</body>
</html>
);
}
export default function App() {
return (
<Document>
<Outlet />
</Document>
);
}
Copy the code
Now we can see the loaded JS file and the page will not refresh when clicking joke to get the joke
Also, when you now have the ErrorBoundary component printing an error with console.error, it will not only print the error message on the server side, but also in the browser.
Form
HTML’s native
Prefetch
If a user moves the mouse over a link, it indicates that the user may want to jump to that page, so we can prefetch the corresponding page, so that the user will feel fast when clicking the jump. Doing this in Remix is as simple as adding a property prefetch to the component
From the TypeScript file for Remix, you can see that this property has three values to fill in
When you mouse over, you will find the JS file corresponding to the successful prefetch in network
UI Interaction optimization
The example on the official website also describes how to use useTransition to optimize the interactive display of the UI interface after users click to submit data. For details, you can directly click on it to see, without elaborating the description here.
The deployment of
The tutorial on the website uses fly.io to deploy the project, and anyone interested can sign up for an account to try it out
conclusion
Remix as a full-stack framework has several advantages
- Natural nesting routines by advantage
- All kinds of error boundary handling
- Close to the native WEB, you don’t need to invest too much in learning React, but you still need to learn the basics
- SEO friendly
- Automatically split code without tedious configuration
After all, Remix has just released version 1.0, and there are still few resources such as ecology and tutorials. However, I believe that Remix can get 3 million DOLLARS of investment after closing the subscription model as an open source project. Many people still have confidence in this framework, after all, the SEO problem of server rendering. Many people develop with Next or Nuxt. As a full-stack framework, Remix can also provide server-side rendering, and it is integrated. We believe that more people will choose Remix framework in the future.
From the front end to the full stack, I think many people have thought about this problem. To achieve the full stack, most of the time, it means that you need to learn a new language, which has become the obstacle for many people.
Remix solves the language problem and will be an attempt by many front-end developers to move to the full stack. It is believed that the future development of front-end is not only this.