A simple CRUD app in Remix
Friday, June 25, 2021
Intro
Today is the first post of a multi part series I hope to show building the same simple CRUD app across a few javascript frameworks with a final post contrasting the experience.
Today we dive in using Remix.
Rough App Idea
A simple app that has the ability to Create Read Update Delete a central storage of web links. Essentially your bookmark bar, but with ability to track extra notes on why that link is important to have a reference.
Disclaimer
You should always be thinking right tool, for the right job - we're just scratching the surface here but clearly just a markdown file locally to reference would suffice for what we are building today, but this gives us a small enough thing to bite off and compare against other frameworks to compare and contrast.
Let's get Started
I'll be sharing the repo and demo app to see the code and final results rather than calling out the next few steps. Ideally they are pretty much the same in each app, and the reason I am using standard tools that can be shared to compare.
A rough outline of the project though is shaped like this:
- Project Setup: Initialize the project with the framework and dependencies
- ORM Setup: Setup the models in the ORM
- Pages and API: Create pages and API to interact with the ORM
- Reaction: Some initial reactions
Project Setup
Remix
Installing Remix is a pretty smooth process. Just make sure you have a license and they've got a nice Tutorial to get you going
Linting / Formatting
Every project I generally drop in Prettier and Eslint to keep consistent with formatting and catching common formatting based mistakes
Database connectivity
For this app we will store information to a database, we'll leverage Prisma as our ORM to simplify that interactivity.
Styling Design System
Throwing in chakra-ui as a quick styling tool so I can test against next, gatsby, create react app - so using the same dependencies in each should level the playing field.
ORM Setup
This is simple enough, we're going to have just one model to work with, following the prisma documentation and prompts and we are off to the races, for context here is our schema file
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model WebLinks {
id Int @id @default(autoincrement())
name String @unique
link String
notes String
category String
}
Pages and API
Viewing
Remix has you structure your app around routes. If you were going to have a bunch functionality maybe we would put our list view in something like app/routes/links/index.tsx
but we'll just use the root file that was created from the generator for simplicty (app/routes/index.tsx)
As you see in the example pages have the ability to export a LoaderFunction
which runs in the server processing of the request. Here we can pull the list of links through our prisma client and send it to the page. I'd say this is comparable to getServerSideProps
from Next.js.
An interesting twist here is Remix can have javascript completely disabled, and this will still work. As you can turn off hydration and it will just server render the content and pass it along without any JS bundle at all. I believe this is possible in Next.js too but for those that are trying to ship less javascript. This is an appealing option to keep me building in react, but removing the bundle impact on our consumers. Without forcing it fully down the statically pre-built generated path.
Adding
Being routing first, you can create urls that really explain what we're doing easily. So we're going to create a way to create a new
entry for our WebLinks
model. All it takes to make this a valid path is to create a file. It can be weblinks.new
or weblinks/new
whichever grouping your team prefers. Simply export a default function with your rendered content, and you've got a route up and running.
Our page quickly styled using Chakra
import {
Input,
Center,
Textarea,
Container,
Heading,
Button,
VStack,
HStack,
} from "@chakra-ui/react";
import type { ReactElement } from "react";
export default function WebLinkNew(): ReactElement {
return (
<Container maxW={"6xl"} mt={10}>
<Center>
<Heading pb={2}>Create a new Link</Heading>
</Center>
<form method="post">
<Center>
<VStack>
<Input placeholder="Name" required type="text" name="name" />
<Input
placeholder="Category"
required
type="text"
name="category"
/>
<Input placeholder="Url" required type="text" name="link" />
<Textarea
placeholder="Notes about the link"
required
rows={10}
name="notes"
/>
<HStack>
<Button variant="ghost" colorScheme="blue" as="a" href="/">
Cancel
</Button>
<Button colorScheme="blue" type="submit">
Create New Link
</Button>
</HStack>
</VStack>
</Center>
</form>
</Container>
);
}
You'll notice we've got a form there but its not posting to an API or anything like you'd expect. Submittig the form though you'll see some helpful information letting us know how we can act on the submission
You made a POST request to http://localhost:3000/webLinks/new but did not provide an
action
for route "routes/weblinks.new", so there is no way to handle the request.
In other frameworks you'd probably spin up an API endpoint and post the data there, remix gives you the ability to handle the post in the same file that you are building the form by exporting an ActionFunction
. This starting to make me feel closer to my Ruby on Rails roots, but with the view also in the mix.
After adding our ActionFunction
, we've quickly ramped up with the ability to add records to our system and we can easily see the view and api side in one place.
import { PrismaClient } from "@prisma/client";
import { ActionFunction, Link, redirect } from "remix";
import {
Input,
Center,
Textarea,
Container,
Heading,
Button,
VStack,
HStack,
} from "@chakra-ui/react";
import type { ReactElement } from "react";
export const action: ActionFunction = async ({ request }) => {
const body = new URLSearchParams(await request.text());
const name = body.get("name") as string;
const category = body.get("category") as string;
const link = body.get("link") as string;
const notes = body.get("notes") as string;
const prisma = new PrismaClient();
async function main() {
await prisma.webLinks.create({
data: {
name,
category,
link,
notes,
},
});
}
await main()
.catch((e) => {
throw e;
})
.finally(async () => {
await prisma.$disconnect();
});
return redirect("/");
};
(original render function)
I'll throw in Editing and Deleting soon enough in the demo app, but this gives you a quick of the "C" and "R" of our "CRUD" app.
Demo
Reactions
- Using the React ecoystem to build a product that results in no JS bundle is an interesting value prop. In practice I wonder where this is the right fit - but it definitely helps pressure test should you include ALL that javascript, just to toggle a button?
- I'm looking for what makes this different than Next.js, at first glance they seem to offer a lot of the same functionality with just some different patterns. That's the some of the original driver of writing this article.
Struggles
Remix is still in Beta - so its expected to be a little tricky, I hit a few rough edges, but have worked through them. So far there are two that stick out the most to me.
yarn vs npm
More than once I would get mixed up depencies after running yarn install
. So much so that I figured I would go back to npm
like most of the doc examples from remix are written in. Since the switch I haven't hit one issue with installing dependencies getting linting errors or failures, so seems like yarn is less supported (at this time). This could totally be just my setup, but I noticed the problem pretty consistently when adding packages - my original assumption is because of the alternative package distribution for key checking etc.
Mismatching some HTML can easily crash your server :(
Being more familiar with Create React App
and Next.js
I believe the server is generally a bit more forgiving with my fat finger ways. Quite a few times I ended up here, and wondering why my app wasnt updating.
> remix-app-template@ dev /Users/davidlarrabee/workspace/playground/remix-web-link-portal
> remix run
Watching Remix app in development mode...
Remix App Server started at http://localhost:3000
💿 Built in 103ms
💿 File changed: app/routes/index.tsx
💿 Rebuilding...
> route-module:/Users/davidlarrabee/workspace/playground/remix-web-link-portal/app/routes/index.tsx:44:6: error: Expected closing tag "div" to match opening tag "h2"
44 │ </div>
╵ ~~~
route-module:/Users/davidlarrabee/workspace/playground/remix-web-link-portal/app/routes/index.tsx:28:7: note: The opening tag "h2" is here
28 │ <h2>Important Links
╵ ~~
> route-module:/Users/davidlarrabee/workspace/playground/remix-web-link-portal/app/routes/index.tsx:47:0: error: Unexpected end of file
47 │
╵ ^
(....)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! remix-app-template@ dev: `remix run`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the remix-app-template@ dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/davidlarrabee/.npm/_logs/2021-06-23T16_23_50_732Z-debug.log
Not something that I am surprised about, being so early on - but just calling it out there for the faint at heart.
What's next?
I'd love to dive in more with Remix -just have to come up with some other interesting challenges or ideas to chase. Ideally I'll write future posts comparing this same sample app in some alternatives like Next.js, Gatsby, and just straight up Create React App. I'll keep you posted!