Is Remix your next framework?
Michael Jackson
Ryan Florence
Kent C Dodds
Remix is driven by community leaders
Reaching the goal
Perhaps JavaScript should not be the starting point for the developer, but should be thought of as an extension.
Works without JavaScript
CSR vs SSR vs SSG vs ISR
Where to render
Client Side Rendering (CSR)
Server Side Rendering (SSR)
Server Side Generation (SSG/ISR)
Remix
Remix
Using what we know
It's just React
Without
the hard parts
React without
the hard parts
Learn Remix, Accidentally Learn the Web
Full Stack
Remix vs NextJS
Remix is all about
app
├── root.jsx
└── routes
├── accounts.jsx
├── dashboard.jsx
├── expenses.jsx
├── index.jsx
├── reports.jsx
├── sales
│ ├── customers.jsx
│ ├── deposits.jsx
│ ├── index.jsx
│ ├── invoices
│ │ ├── $invoiceId.jsx
│ │ └── index.jsx
│ ├── invoices.jsx
│ └── subscriptions.jsx
└── sales.jsx
Defining routes
/sales/invoices/102000
- root.jsx
- routes/sales.jsx
- routes/sales/invoices.jsx
- routes/sales/invoices/$invoiceId.jsx<Root>
<Sales>
<Invoices>
<InvoiceId />
</Invoices>
</Sales>
</Root>Time to First Byte (TTFB)
First Contentful Paint (FCP)
Cumulative Layout Shift (CLS)
Largest Contentful Paint (LCP)
Time To Interactive (TTI)
How can we describe performance
Rendering of a standard SPA
Parallel data fetching
Server side rendering
Server side rendering with caching
import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
import { db } from "~/db.server";
export const loader: LoaderFunction = async ({ params }) => {
return json(
await db.product.findMany({
where: { categoryId: params.categoryId },
})
);
};
export default function ProductCategory() {
const products = useLoaderData();
return (
<div>
<p>{products.length} Products</p>
{/* ... */}
</div>
);
}Route with data fetching
function MyForm() {
const [inputs, setInputs] = useState({});
const handleChange = (event) => {
const name = event.target.name;
const value = event.target.value;
setInputs(values => ({...values, [name]: value}))
}
const handleSubmit = (event) => {
event.preventDefault();
console.log(inputs);
};
return (
<form onSubmit={handleSubmit}>
<label>Enter your name:
<input type="text"
value={inputs.username || ""}
onChange={handleChange}
/>
</label>
</form>
)
}React Form
The power of Remix
GET /users
GET /users?name=peter
import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
export const loader = async ({ request }: LoaderArgs) => {
const url = new URL(request.url);
const search = new URLSearchParams(url.search);
const query = search.get('query') || '';
const data = [....];
return json({ query, data });
};
export default function Users() {
const { data, query } = useLoaderData();
return (
<h1>Search page</h1>
<form method='get'>
<input type='text' name='query' defaultValue={query}/>
</form>
)
}Filter form (get)
import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const user = await db.save.user(formData);
return redirect(`/users/${user.id}`);
};
export default function NewUser() {
return (
<form method="post" action="/search">
<input name="name" type="text" />
<input name="email" type="email" />
<button type="submit">Submit</button>
</form>
)
}Mutation form (post)
// Zod validation schema
export const formSchema = z.object({
name: z.string(),
email: z.string().email().optional()
});
export const action = async ({ request }) => {
const formPayload = Object.fromEntries(await request.formData());
try {
const formValues = formSchema.parse(formPayload);
const user = await db.save.user(formValues);
return redirect(`/users/${user.id}`);
} catch (error) {
return {
formPayload,
errors: (error as ZodError).flatten().fieldErrors,
};
}
};
Form validation - action
export default function NewUser() {
const actionData = useActionData();
return (
<form method="get" action="/search">
<input name="name" type="text" />
{actionData?.errors.name && (
<span className="text-red-600">{actionData?.errors.name[0]}</span>
)}
<input name=email" type="email" />
{actionData?.email.name && (
<span className="text-red-600">{actionData?.errors.email[0]}</span>
)}
<button type="submit">Submit</button>
</form>
)
}Form validation - handle errors
// root.aspx
export function ErrorBoundary({ error }) {
return (
<html>
<head>
<title>Oh no!</title>
<Meta />
<Links />
</head>
<body className='m-4'>
<h1 className='text-2xl'>Something went wrong!</h1>
<p>{error.message}</p>
<Scripts />
</body>
</html>
);
}Global Error handling
// my route component
export default () => {
return (
<div>...</div>
)
}
// Error Boundery Component
export function ErrorBoundary({ error }) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
}Route Error handling
// We can throw a 404 error
export const loader = async ({ params }: LoaderArgs) => {
const id = params.id;
const category = await getDb().categories.findOne({ _id: new SafeObjectId(id) });
if (!category) {
throw json({ message: 'Resources not found' }, 404);
}
return json({ category });
};Catch Error (http status errors)
// Status Catch Boundery
export function CatchBoundary() {
const { data, status } = useCatch();
return (
<div>
<p>Resources not found [{status} - {data.message}]</p>
</div>
);
}Software Spectrum
So less code
faster UX
better DX
Is Remix
your next
framework?
Want to learn more?