DEV Community

Cover image for Transforming Fullstack Development with Remix: A React Comparison
Adrianna V.
Adrianna V.

Posted on • Updated on

Transforming Fullstack Development with Remix: A React Comparison

In my recent livestream, I discussed the crucial concept of Fullstack Data Flow in the Remix Framework. I mistakenly referred to it as the Remix Full stack cycle, so I want to clarify. This overarching flow within Remix is essential to grasp as you dive into this framework.

I aim to compare the new approach to building with React in Remix with our traditional way of using React alone, often with multiple libraries. This comparison should be particularly beneficial for React developers who are still undecided about adopting full stack frameworks like Remix.

Currently, Remix is built around React Router to enhance Server Side Rendering capabilities. It offers built-in support for nested routes, providing a more manageable structure through a single directory: app/routes.

The intricacies of building routes in Remix warrant their own blog post, so I won't dive into that here. I appreciate the variety of options available though, and many engineers have strong opinions about their preferred approach.

Table of Contents

  1. Let’s talk about the flow of data
  2. React alone in comparison

Let’s talk about the flow of data

The flow is split into three phases:
Loaders → Components → Action

Remix FullStack Data Flow made on Miro

Loaders

In Remix, loader functions are used to fetch data on the server-side. They should always be defined in route files.

Keep in mind by default in Remix we are rendering our data from the server or also known as Server Side Rendering (SSR)

Example:

export const loader = async ({ params }) => {
  const data = await fetchData(params.id);
  return json(data);
};
Enter fullscreen mode Exit fullscreen mode

Components

In Remix, components receive the data fetched from loader functions to be rendered. Components access this data with useLoaderData hook.

Example:

import { useLoaderData } from "@remix-run/react";

export const loader = async ({ params }) => {
  const data = await fetchData(params.id);
  return json(data);
};

export default function Post() {
  const data = useLoaderData();
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Actions

Action functions handle form submissions, validations, and other mutations. They are called when a form is being submitted, allow the server to process the data, update if successful or return validation errors that indicate to the user what failed.

Example:

export default function NewPost() {
  const actionData = useActionData();

  return (
    <div>
      <h1>Create a New Post</h1>
      {actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>}
      <Form method="post">
        <div>
          <label>
            Title: <input type="text" name="title" />
          </label>
        </div>
        <div>
          <label>
            Content: <textarea name="content" />
          </label>
        </div>
        <button type="submit">Create Post</button>
      </Form>
    </div>
  );
}

export const action = async ({ request }) => {
  const formData = await request.formData();
  const newPost = await createPost(formData);
  return redirect(`/posts/${newPost.id}`);
};
Enter fullscreen mode Exit fullscreen mode

As we’ve seen in Remix how we fetch data, render that data in our components, and handle data mutations through form handling, the overall pattern is not that different from when we used React alone but as you can see we are are rarely using our regular hooks useState or useEffect.

React alone in comparison

This would compare to Remix’s loader function and useLoaderData hook.

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const Post = ({ postId }) => {
  const [post, setPost] = useState(null);

  useEffect(() => {
    axios.get(`/api/posts/${postId}`)
      .then(response => {
        setPost(response.data);
      })
      .catch(error => {
        console.error("There was an error fetching the post!", error);
      });
  }, [postId]);

  if (!post) return <div>Loading...</div>;

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
};

export default Post;

Enter fullscreen mode Exit fullscreen mode

That’s not bad but now there is less separation between the data and how we go about fetching that data. We’ve been used to that pattern for a while and got used to it.

Now let’s compare Remix’s action function and useActionData to using React standalone

const PostForm = () => {
  const [formData, setFormData] = useState({ title: '', content: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};

    if (!formData.title.trim()) {
      newErrors.title = 'Title is required';
    }

    if (!formData.content.trim()) {
      newErrors.content = 'Content is required';
    }

    setErrors(newErrors);

    return Object.keys(newErrors).length === 0;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    if (!validate()) {
      return;
    }

    try {
      const response = await axios.post('/api/posts', formData);
      console.log('Post created', response.data);
    } catch (error) {
      console.error('There was an error creating the post!', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          name="title"
          value={formData.title}
          onChange={handleChange}
          placeholder="Title"
        />
        {errors.title && <p style={{ color: 'red' }}>{errors.title}</p>}
      </div>
      <div>
        <textarea
          name="content"
          value={formData.content}
          onChange={handleChange}
          placeholder="Content"
        />
        {errors.content && <p style={{ color: 'red' }}>{errors.content}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

export default PostForm;
Enter fullscreen mode Exit fullscreen mode

Yes, we can definitely find ways to consolidate this and use third party libraries that help with forms like react-final-form or Formkik but can we be honest that following Remix’s pattern helps us write cleaner code from the beginning? There’s a lot you don’t need to do in Remix because of it’s intuitive nature to rely on not only React Router but on the browser’s natural flow as well.

If you made it this far, thanks for reading. I talk more about Remix than I write about it. I host a livestream titled Astrology & JavaScript Series on Wednesdays at 1pm ET on X and Twitch. I’m building a tool that helps you understand astrology more by learning the foundational blocks first.

I use Typescript within Remix and good ole Tailwind. I pull all the required data from DivineAPI (Astrology & JavaScript Series Sponsor!). They provide me all the information I need, working with their Western Astrology API. I talk often about my process, go through the code and do some live-coding as well.

Top comments (0)