Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

Recently, the emergence of remix has caused a huge response on Twitter, and many frontend communities are discussing it. Coincidentally, we also have an internal tool to develop, so I decided to give it a try. It feels very similar to next.js, mainly based on route-based approach, using standardized functions like getServerSideProps to fetch data on the server for SSR.

Remix was created by Ryan Florence, the author of react-router. Initially, it was intended to be a paid framework, but later it became open source. Although some people might think, "Oh, another framework," Remix does have its unique features. If you have read the documentation, you will notice that Remix is particularly focused on form handling, which is the biggest difference between Remix and other SSR frameworks. Looking back now, handling forms can be confusing for beginners in frontend development.

Wait, but doesn't the browser refresh when a form is submitted? That doesn't meet the requirements!

Remix has put a lot of effort into form handling. In addition to using forms to implement asynchronous requests, if JavaScript is disabled, it will fallback to the browser's built-in form handling. Although it requires a page refresh, at least the submission functionality still works. (And sometimes, directly submitting the form and refreshing the browser actually provides a better experience than asynchronous handling 🤔)

This article will not delve into all the features of Remix, but will focus on form handling and data retrieval.

What are the benefits of form handling?

You don't need to write a lot of JavaScript code or use additional variables (or state) to store input values. For example, in the following example:

<form action="/my/api" method="post">
  <input name="user_name" type="text" />
  <input name="world" type="text" />
  <button type="submit">
    Submit
  </button>
</form>

By default (without any intervening code), when the button is clicked, the browser automatically sends a request to /my/api using the POST method, and submits the user_name and world fields as form data.

However, the default behavior requires server-side assistance to fully implement. For example, if the server only returns JSON, the user's page will stay on the same page. Therefore, in most applications, the server usually returns a 301 redirect to another page, such as a success page or a failure page.

Sometimes, simply refreshing the entire page can make things much simpler because you don't have to maintain various states and manipulate the history in the frontend. However, for applications that require real-time messaging or chat, where real-time interaction is emphasized, page refresh is not ideal.

So how do we turn a form into an asynchronous operation? After preventing the default behavior through the submit event, you can write code to submit the form:

<form action="/my/api" method="post" onSubmit={e => {
  e.preventDefault()
  // call API!
}}>
  <input name="user_name" type="text" />
  <input name="world" type="text" />
  <button type="submit">
    Submit
  </button>
</form>

The biggest advantage of this approach is that the entire form operation is handled by the browser mechanism, and in the browser, you can easily access the form values using FormData:

<form action="/my/api" method="post" onSubmit={e => {
  e.preventDefault()
  const formData = new FormData(e.currentTarget)
  const userName = formData.get('user_name')
  const world = formData.get('world')
}}>
  <input name="user_name" type="text" />
  <input name="world" type="text" />
  <button type="submit">
    Submit
  </button>
</form>

This way, you can obtain the input values without the need for additional refs or states. If you don't utilize the native form mechanism, in React, you might write something like this:

const Component = () => {
  const [value1, setVal1] = useState()
  const [value2, setVal2] = useState()
  const [value3, setVal3] = useState()
  return <>
    <input value={value1} onChange={handleChange} />
    <input value={value2} onChange={handleChange} />
    <input value={value3} onChange={handleChange} />
  </>
}

As you can see, as the number of input fields increases, the number of useState also increases, making it more cumbersome to write. Of course, this part can also be abstracted to solve the problem, but it adds another layer of abstraction and overhead.

However, once the form is turned into an asynchronous operation, there are many states to consider:

  • Loading: The form is being submitted, so all fields should be disabled to prevent user interaction.
  • Form validation error: The field values need to be retained and error messages displayed.
  • API success: Display corresponding prompts and clear the form.

Remix provides API support for both form submission and asynchronous form applications. I like this approach because many times, we don't really need to go through the trouble of handling asynchronous operations. Writing a lot of code just for simple form submission is unnecessary.

Synchronous (Using native form mechanism)

In the native scenario, as long as the form content is defined step by step, there is no need to declare any state.

// routes/users/index.js
export default function UserProfile() {
  return <form method="post" action="/users">
    <label>
      Name: <input type="text" name="userName" />
    </label>
    <label>
      Age: <input type="text" name="age" />
    </label>
    <button>Submit</button>
  </form>
}

On the server-side, you need to add the corresponding handling. In Remix, the approach is to add a function called action in the same file:

import { redirect } from 'remix';

export async function action({ request }) {
  const formData = await request.formData()
  const user = await createUser(formData)
  return redirect(`/users/${user.id}`)
}

// routes/users/index.js
export default function UserProfile() {
  return <form method="post" action="/users">
    <label>
      Name: <input type="text" name="userName" />
    </label>
    <label>
      Age: <input type="text" name="age" />
    </label>
    <button>Submit</button>
  </form>
}

When the HTTP method is not GET, the action function will be called, so you need to check the HTTP method inside the action function.

The redirect function provided by Remix is used to redirect to the users/id page on the server-side. It is important to note that this redirection happens on the server-side, not using history.push. If there is form validation or server-side error, you can use useActionData to retrieve the data returned by the server.

export async function action({ request }) {
  const formData = await request.formData()  
+  if (someError) {
+    json({ error: message })
+  }
  return redirect(`/users/${user.id}`)
}

// routes/users/index.js
export default UserProfile() {
+ const actionData = useActionData()
  return <form method="post" action="/users">
    <label>
      Name: <input type="text" name="userName" />
    </label>
    <label>
      Age: <input type="text" name="age" />
    </label>
+   {actionData.error && <p>{actionData.error}</p>}
    <button>Submit</button>
  </form>
}

Asynchronous

For asynchronous requirements, Remix provides the Form component for developers to use. To display the current form submission status, you can use useTransition:

import { Form, useTransition } from 'remix'

// routes/users/index.js
export default function UserProfile() {
  const actionData = useActionData()
  const transition = useTransition()
  
  return <form method="post" action="/users">
    <label>
      Name:
      <input
        type="text"
        name="userName"
        disabled={transition.state === 'submitting'}
      />
    </label>
    {actionData.error && <p>{actionData.error}</p>}
    <button>Submit</button>
  </form>
}

In addition to these two mechanisms, you can also use useSubmit to determine when to submit the form. Whether it is synchronous or asynchronous, Remix's approach to form handling is that when you submit a form, it goes through a series of operations before redirecting to a certain page.

Data Retrieval Mechanism - Loader and Fetcher

In web applications, there are mainly two common scenarios for data retrieval:

  1. When the user accesses a page through <a> or the address bar, the page data is retrieved, such as blog articles.
  2. When the user performs certain actions, an API call is made to retrieve data, such as clicking on a comment section to fetch data.

For the first scenario, Remix provides a loader mechanism, which only renders on the server-side. Therefore, when the components are rendered, they already have the data, so there is no need to handle loading and rendering data after the fact.

For the second scenario, Remix provides a fetcher mechanism. You can use useFetcher to perform mutations and data retrieval:

const MyComponent = () => {
  const fetcher = useFetcher()
  useEffect(() => {
    fetcher.load('/comments')
  }, [])

  if (fetcher.state === 'loading') {
    return <p>loading...</p>
  }

  return <div>
    <Comments comments={fetcher.data.comments} />
  </div>
}

Fetcher can not only retrieve data but also perform mutations:

const MyComponent = ({ content }) => {
  const fetcher = useFetcher()
  
  return <fetcher.Form method="post" action="/comments">
    <input type="text" name="content"></input>
    <button type="submit">Submit</button>
  </fetcher.Form>
}

Behind the scenes, Remix handles the mentioned operations:

  • When the submit button is clicked, the state is changed to 'submitting' to facilitate loading UI.
  • After receiving the data, you can access it using fetcher.data.
  • fetcher.submit directly submits the form content, but does not perform redirection (it actually calls the API and handles various states).

Edge Computing

Another point that Remix emphasizes is the concept of Edge server (CDN), which means deploying servers in various nodes closer to the users to reduce network traffic.

Indeed, if the user's device performance is no longer the bottleneck, then the only factor that affects the experience is the network speed. Edge computing refers to deploying servers to nodes closer to the users, thereby reducing the routing of packets. However, for some applications that target local users, the difference may not be significant.

Conclusion

I like the approach of Remix because its emphasis on form handling reminds me of the idea I had before. Using the native form mechanism can save a lot of hassle in declaring value states, and the form handling mechanism can also handle simple application scenarios without the need for various asynchronous APIs. In situations where development time is limited, there are many considerations when dealing with asynchronous operations, and often, if the experience is not handled well, it may be worse than traditional forms.

Remix takes into account various scenarios. Without JavaScript, it can use the native form mechanism. If real-time interaction is needed, the Form component can be used for assembly. If more SPA-like interactive scenarios are required, fetcher can be used. For data processing, which is the most important concern in frontend development, Remix solves half of the problem.

Prev

ATOMIC_BLOCK in avr-libc

Next

String Processing in the C Language

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.