Remix Concepts in Next.js
January 15, 2022
Remix is a new full stack web framework built on-top of React Router. It's been on my radar for quite a while so I was eager to dive into the getting started tutorial after they launched their first stable release (v1).
My initial impressions are very possitive. The framework has certainly come a long way in a short amount of time. On top of that, it manages to innovate in a few areas that I wanted to share some thoughts on.
Next.js, my go-to framework
Next.js has served as the baseline for all my web projects in the past 4-5 years. Every new release continues to surprise and impress me. It's developed into a highy polished and versetile framework and there's really no going wrong with it. I especially appreciate how closely it tracks the official React roadmap.
Head to head
Remix borrows a lot from Next.js (the file-based routing, module export API, etc.) However, it innovates on top of that foundation to the point that I'm starting to feel just a little bit jealous. That's why I decided to implement Remix's Developer Blog tutorial - but in Next.js to get a better idea of what I'm really missing out on.
You can see the end-result in the following repo:
- Remix: robinandeer/remix-concepts-in-nextjs/tree/remix
- Next.js: robinandeer/remix-concepts-in-nextjs
Let's break down the main areas where I see Remix innovating so far, and how to accomplish the same things in Next.js.
Form handling
If you are not yet familiar with forms in Remix, I recommend first getting up to speed on Data writes.
I've really enjoyed working with forms in Remix. By relying on web standards, they are able to simplify things while enabling progressive enhancement. I've struggled a lot with forms in React so I'm happy they adress this topic.
What I especially like is how doing form handling and validation server-side is a great separation of concern. Bundling some version of Joi in the browser to validate inputs client-side never felt quite right to me. And now I think I will never do so again.
Remix-forms in Next.js
So can we achieve something like this in Next.js? It's not so far off as you might think. I believe we have all the building blocks. We can even make the form work with JavaScript disabled - just like in Remix!
Here's the solution I came up with in a Page-component that renders a simplified "new post form":
function NewPostPage() {
const {submission, errors, formProps} = useForm({
action: '/api/posts',
});
return (
<form {...formProps}>
<p>
<label>
Post title: {' '}
{errors?.title ? <em>Title is required</em> : null}
<input type="text" name="title" />
</label>
</p>
<p>
<button type="submit">
{submission ? 'Creating post...' : 'Create post'}
</button>
</p>
</form>
);
}
Full example: src/pages/admin/new.tsx
The useForm
hook
export function useForm(action) {
const [{submission, errors}, setFormState] = useState(
{submission: false, errors: null}
);
const handleSubmit = async (event) => {
event.preventDefault();
setFormState({submission: true, errors: null});
const response = await fetch(action, {
method: 'post',
body: new FormData(event.currentTarget),
});
if (response.ok) {
setFormState({submission: false, errors: null});
if (response.redirected) {
router.push(response.url);
}
} else {
const {errors} = await response.json();
setFormState({submission: false, errors});
}
};
const formProps = {
action,
method,
onSubmit: handleSubmit,
};
return {submission, errors, formProps};
}
Full example: src/use-form.ts.
First we need a generic hook that does the following:
- Submits the form data to a given endpoint
- Keeps track of some state telling us when the form is "submitting"
- Returning validation errors reported by the server
- Forwarding any redirects that the server responds with
This replaces the basic functionality provided through the Remix-hooks as well as the <Form />
component.
Using an API route to handle form submission
export default async function newPost(req, res) {
const {title} = await getFormData(req);
const errors = {};
if (!title) {
errors.title = true;
}
if (Object.keys(errors).length) {
return res.status(400).json({errors});
}
await createPost({title, slug, markdown});
res.redirect('/admin');
}
Full example: src/pages/api/posts/index.ts.
To replace the action
-handler from Remix we use an API route. It's responsible for handling the form submission and validating the input. This is essentially the same as in Remix although we miss out on the benefits that come from co-location with the page component. We also need to manually polyfill the FormData
API (I used formiddable
) to be able to use it in Node.
SSR all the way
Again, start by making sure you know how Remix handles Data Loading.
If you chose the red JAMStack-pill, you might be sceptical about server-rendering. However, Ryan, Michael, and Kent makes a convincing argument that SSR is the way to go. There's some liberty in that. Instead of deciding between SSR, SSG, or ISG for each page I can get on with my life and focus on more domain-specific problems.
The theme here really is a return to how things used to work; server rendered and relying on web standards. However, React is just one hook away to progressively add interactivity to any page. A quite elegant by-product is how much of the Remix site works even with JavaScript disabled. Especially for projects that are less app and more website, this approach makes a great deal of sense.
For some nuance, Lee Robinson provides some counter argument in this Twitter-thread claiming the benefits or static hosting when availability is your primary concern:
1/ How can I optimize my frontend for the best availability?
ā Lee Robinson (@leeerob) January 21, 2022
I want to guarantee customers will almost never see a broken page.
What would the architecture and infrastructure look like?
My takeaway is still that SSR is a viable option for a variety of scenarios and can significantly reduce client-side complexity. Many times it's worth loading data on the server for no other reason than to avoid "spinnmageddon".
Next.js leading the way
export const getServerSideProps = async () => ({
props: {
posts: await getPosts(),
},
});
export default function Posts({posts}) {
return (
<ul>
{posts.map(post => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
);
}
Full example: src/pages/index.tsx
Next.js already has excellent SSR-support through getServerSideProps
. The main difference from the Remix loader
-function seems to be that the data is accessed through a hook in Remix and directly through page props in Next.js.
Nested routes
If you need a refresher on this topic, take a look at Remix's guide on Layout Routes.
I remember being introduced to nested routing in Ember.js a long long time ago. It always felt like a powerful concept if you were building e.g. an email client with a standard sidebar layout. However, in projects I've worked on I haven't seen the use case pop-up more than a few times. It could be because these sites were built with mobile-first in mind where you naturally focus on pages that have a single concern. Therefore, I wouldn't say it's a killer feature for me.
Using layout components
My Next.js solution uses a simple layout component.
import AdminLayout from '~/components/admin-layout';
export default function AdminPage({posts}: Props) {
return (
<AdminLayout posts={posts}>
<p>
<Link href="/admin/new">Create a New Post</Link>
</p>
</AdminLayout>
);
}
Totally doable and with a lot less magic than the Remix version. However, for deeply nested routes it does get complicated to pass all page props while keeping the solution DRY.
Conclusion
That was a lot! Thanks for sticking with it to the end. I certainly learned a lot from just going through the simple Remix getting started tutorial. There's plenty of great ideas to get inspired by and some you can take advantage of without fully committing to a new framework.
If you wanna dig deeper into the "remixed" Next.js-version of the developer blog, do check out the full project repo.
I will keep an eye on Remix for sure. They've come a long way but Next.js is still the more mature option (proven, huge community etc.) and more feature complete (Fast Refresh, i18n) which is why I will probably stick with it for the time being. However, I will keep learning and stealing the best Remix ideas to improve my own projects.
Remix started with the hard part (the bridge between the client and server) and is working its way further up and down the stack from there.@remix_run isn't done, but the hard part is. And you can feel it when you're building/using remix apps.https://t.co/EYM4eajR41
ā Kent C. Dodds šæ (@kentcdodds) January 16, 2022