x-technology

XTechnology - Technology is beautiful, let's discover it together!

Back to the roots with Remix

The modern web would be different without rich client-side applications supported by powerful frameworks: React, Angular, Vue, Lit, and many others. These frameworks rely on client-side JavaScript, which is their core. However, there are other approaches to rendering. One of them (quite old, by the way) is server-side rendering entirely without JavaScript. Let’s find out if this is a good idea and how Remix can help us with it?

Prerequisites

Agenda

Alex Korzhikov

alex korzhikov photo

Software Engineer, Netherlands

My primary interest is self development and craftsmanship. I enjoy exploring technologies, coding open source and enterprise projects, teaching, speaking and writing about programming - JavaScript, Node.js, TypeScript, Go, Java, Docker, Kubernetes, JSON Schema, DevOps, Web Components, Algorithms πŸ‘‹ ⚽️ πŸ§‘β€πŸ’» 🎧

Pavlik Kiselev

Pavlik

Software Engineer, Netherlands

JavaScript developer with full-stack experience and frontend passion. He runs a small development agency codeville.agency and likes to talk about technologies they use: React, Remix and Serverless.

Code

Remix Framework

About Remix

a modern full stack web framework

import { json } from "@remix-run/node"; // or cloudflare/deno

export async function loader() {
  const res = await fetch("https://api.github.com/gists");
  const gists = await res.json();

  return json(
    gists.map((gist) => ({
      description: gist.description,
      url: gist.html_url,
      files: Object.keys(gist.files),
      owner: gist.owner.login,
    }))
  );
}

export default function Gists() {
  const gists = useLoaderData<typeof loader>();

  return (
    <ul>
      {gists.map((gist) => (
        <li key={gist.id}>
          <a href={gist.url}>
            {gist.description}, {gist.owner}
          </a>
          <ul>
            {gist.files.map((key) => (
              <li key={key}>{key}</li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

Rendering Approaches

Rendering Approaches

Remix Rendering Approaches

Remix Example Application

Remix Blog Tutorial

How Remix Works

a compiler for React Router

How Compiler Works

// https://github.com/remix-run/examples/blob/main/_official-blog-tutorial/app/routes/posts/index.tsx
import { json } from "@remix-run/node"
import { Link, useLoaderData } from "@remix-run/react"

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

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

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>()
  return (
    <main>
      <h1>Posts</h1>
      <Link to="admin" className="text-red-600 underline">Admin</Link>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link to={post.slug} className="text-blue-600 underline">{post.title}</Link>
          </li>
        ))}
      </ul>
    </main>
  )
}
import { createPost } from "~/models/post.server"

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

  const title = formData.get("title")
  if (!title) return json({ error: "Title is required" })

  await createPost({ title })

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

export default function NewPost() {
  const transition = useTransition()
  const isCreating = Boolean(transition.submission)

  return (
    <Form method="post">
      <label>
        Post Title:{" "}<input type="text" name="title" />
      </label>
      <button type="submit" disabled={isCreating}>
        {isCreating ? "Creating..." : "Create Post"}
      </button>
    </Form>
  );
}

How Server and Client Work

Remix Routing

app/
β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ about.tsx           // route is based on the static path
β”‚   β”œβ”€β”€ blog/               // route has an additional "/blog" segment in the URL
β”‚   β”‚   β”œβ”€β”€ $postId.tsx     // dynamic params
β”‚   β”‚   β”œβ”€β”€ categories.tsx  // static segments
β”‚   β”‚   └── index.tsx       // index of the "/blog" directory
β”‚   β”œβ”€β”€ $.tsx               // splat route (catch-all routes)
β”‚   β”œβ”€β”€ blog.authors.tsx    // dot delimiter
β”‚   β”œβ”€β”€ blog.tsx            // layout for a regular route
β”‚   └── index.tsx
└── root.tsx

Other Features

remix error boundaries user interface example with message something went wrong

Build

Build

// app/routes/posts/index.tsx
var posts_exports = {};
__export(posts_exports, {
  default: () => Posts,
  loader: () => loader5
});
var import_node7 = require("@remix-run/node"),
    import_react7 = require("@remix-run/react");
var import_jsx_dev_runtime7 = require("react/jsx-dev-runtime"),
    loader5 = async () => (0, import_node7.json)({ posts: await getPosts() });
function Posts() {
  let { posts } = (0, import_react7.useLoaderData)();
  return /* @__PURE__ */ (0, import_jsx_dev_runtime7.jsxDEV)("main", { children: [
    /* @__PURE__ */ (0, import_jsx_dev_runtime7.jsxDEV)("h1", { children: "Posts" }, void 0, !1, {
      fileName: "app/routes/posts/index.tsx"
    }, this),
    /* @__PURE__ */ (0, import_jsx_dev_runtime7.jsxDEV)(import_react7.Link, { to: "admin", ...
    }, this),
    /* @__PURE__ */ (0, import_jsx_dev_runtime7.jsxDEV)("ul", { children: posts.map((post) => ...
    }, this) }, post.slug, !1, {
      fileName: "app/routes/posts/index.tsx"
    ...
}

How Remix Builds

DIY Let’s do Remix today with own hands!

Goals

Steps

First Steps

State of current implementation

Waterfall Loading (Problem)

import { useParams } from "react-router";

type useQueryType = { <T>(x: string): { data: T | null } };
const useQuery: useQueryType = (type) => ({ data: null });

type SalesType = { id: number; overdue: string; dueSoon: string };
type InvoiceType = { id: number; user: string; value: string; details: string };

// URL: /sales/invoices/1
export const InvoicesPage = () => {
  return (
    <div>
      <Invoices />
    </div>
  );
};

const Invoices = () => {
  const { data: invoices } = useQuery<InvoiceType[]>("invoices");
  if (!invoices) return null;
  const { invoiceId } = useParams();
  return (
    <>
        <ul>
          {invoices.map((invoice: InvoiceType) => (
            <li key={invoice.id}>
              <a href={`/invoices/${invoice.id}`}>{invoice.user} {invoice.value}</a>
            </li>
          ))}
        </ul>
        {invoiceId && <Invoice id={Number(invoiceId)} />}
    </>
  );
};

type InvoiceProps = { id: number };
const Invoice = ({ id }: InvoiceProps) => {
  const { data: invoice } = useQuery<InvoiceType>(`invoices/${id}`);
  if (!invoice) return null;
  return (
    <div>
      {invoice.user}: {invoice.details}
    </div>
  );
};

Waterfall Loading (Solution)

Waterfall Loading Solution

Demo - Comparison With NextJS

Summary

Promises

Results

How to Start

Statement

Feedback

Please share your feedback on the workshop. Thank you and have a great coding!

If you like the workshop, you can become our patron, yay! πŸ™

Technologies