<< back

Frontend toolings and good patterns

Background

Recently, the company I am at has been putting in effort to improve our frontend. In the process of this, we have discovered that one of the reasons we are in the muddy water is that we have adopted a bunch of frontend libraries and toolings without real understanding of their intended use cases and used them pretty randomly. Plus, there are anti-patterns.

So in this part, we are going to talk about these tools and how I am thinking about adopting/using them. And how they help in different patterns.

For context, we are doing nothing special - just building a React SPA. As of now, we have below list of libraries that are currently in use.

Data Persistence

The first area we are going to talk about is how long we persist certain data in frontend and where. Of course it's really just either in broswer tooling or in memory but with these different tools, we can categorize them more granularly.

Let's draw up a table for it.

Persistence LocationLifecycle LengthExamples
Local StorageForever until wiped explicitlyN/A
Global State Management toolSame lifecycle of the SPA. Wiped when app reloadsRedux, Jotai
React Context APISame lifecycle of the provider component. Wiped when provider component unmountsN/A
Request CacheDepending on the specific tool but usually has cache key and expiry configured.TanStack Query, URQL

With this table in mind, we can now talk about some anti patterns I have seen.

Storing user profile and some user information in local storage

For a long time, we have been abusing local storage and sort of treated it as the global state management system. This is problematic because for one, it is not, local strage doesn't gets wiped clean when the entire application unmounts. There will be data persisted in the local storage the next time the application gets loaded up and when handled incorrectly, can trip up the entire app. For two, anything you store in local storage is out of your control - it's in client's browser. You can do things like wiping local stroage on user signing out but it still might corner you into a messy situation.

So what's the good pattern here? The answer is straightforward - putting auth information into local storage and using the auth information to load up the user information upon app loading. With only auth information in the local storage (in our case, a standard JWT token), we can ensure everytime an authenticated user loads up the application, the user information is fresh and correct. Then if you need to propagate this user information across the entire app, store it in your global state management system.

Hoisting everything up into the global state

Another instance I have seen is too eagerly hoisting everything up into the global state. One time I heard "we open up a new tab because there was some weird issue if we use react router navigate back to it, it's just a workaround.". Translation: too many things are hoisted up into the global state so we can't properly clean up the global state when trying to navigate back into certain view. If this is the case, maybe context API is the right play as the state it holds gets created and cleaned up with the provider component mounted and unmounted. As long as we placed the provider component in the right place in the component tree, the component lifecycle itself already does the state initialization and cleanup for us.

Coding patterns

Writing pure functions

This is also somewhat related and to some degree, caused by the previous topic of "hoisting everything up into redux".

There are some supposedly reusable functions/hooks that looks like this.

// adding returning type here to emphasize on the fact it returns nothing
function getXXX(): void {
  ...
}

But if you read the code you realize it read some parameters from redux, send some request to fetch the data then store the data back into redux.

This is why I used the word "supposedly" earlier - if there is no comment, no one telling you what it doesn or not readin the code itself, how does the function make sense? not to mention reusability. It's called "get something" but doesn't accept any arguments or returning anything.

To zoom out a little bit, we all heard of the concept of a pure function aka a function with no side effect. Here, we are doing the exact opposite, we are writing a function that's pure side effect LOL.

This type of function defeats the purpose of reusability, readability and maintainabillity as everything is so tightly coupled with the redux store.

To make this better, the getXXX() function could be pure and leave the redux store related stuff out of it.

e.g.

function getXXX(arg1: T): Data {...}
...
const arg1 = store.someDataSlice.something
const data = getXXX(arg1)
dispatch(setData(data))
...

Writing react hooks

Also closely related to the previous topic, we also have issues with blowing up a reusable hook.

Below I am using this picture from Vue's composition API but React hooks and Vue's composition API are conceptually the same and are designed to solve the same problem - which is to group related functionalities closer together. Hooks

A react hook is a function - thus it should be written following almost the same set of good practices for writing a normal function. I say "almost" because you will most likely alter state and thus it probably won't be pure. But otherwise, most of stuff is the same - you accept arguments, making sure the function name is readable, etc. One of the issues I have seen in our code base is we almost NEVER write a hook that accepts arguments, we just grab everything from the redux store - and this is the same problem we have talked about in the previous section.

And there are generally two situations that pushes developers to start writing hooks outside of the component.

  1. The logic is or should be reusable - I don't gotta say much around this one.
  2. Although the logic is component specific and unlikely to be reusable, it's too much code to just pile it up into the component. Thus we extract things out for readability and maintainability purposes. This point is tricky to execute because now it's very tempting to just move everything outside of the JSX stuff into a function and call it a day. But doing this doesn't help you achieve the goal of maintainability and readability. On the contrary, it's worse becuase now the hook logic is even further away from your HTML markups, making the whole component even less readable. So, just because the motivation here is simple, doesn't mean the execution is easy. You still want to look at all the logic in the component and group them, extract them out into potentially mutiple hooks so you are actually making the entire functionality human readable.

React's Reactivity Model - in-component allocation

Note this paragraph is writting on 2025/03/12

React's reactivity model is somewhat backwards comparing to almost any other framework or even vanilla JS. This is something I came to truly realize recently and I think it's worth talking about it.

Before this job, I used Vue for almost 3 years and when I got back to React I just assumed they are similar in reactivity model but that can't be further from the truth.

I am going to use Vue and plain JS in comparision to talk about this section.

In other frameworks, things are presumed not reactive until made reactive. For example, to make an value reactive in vue you need to either make it a ref or a reactive object.

import { ref, reactive } from 'vue'

// within the setup function
const reactiveNumber = ref(1)
const reactiveObject = reactive({})

But react does it the other way around, React re-run the component for EVERY RERENDER. Meaning, if you think of rendering a functional component just as executing a that function. That function gets executed for every change that needs to happen. What this means is every line of code in your component gets execute with every change. More specifically, if you define a function within the component, the memory allocation of that function happens everytime the component updates. This might not seem to be a lot but as the application grows, it will take a toll on the performance. (try google search "why is every react application slow")

This is ultimately the reason React emphasizes so much on those useCallback() and useMemo() hooks. Those hooks under the hook extract the code within them out of the component re-renders and put them back in after re-renders.


const SomeReactComponent = () => {
  // will change with every re-render
  const currentTime = new Date()

  // set one time on mount
  const fistTimeRenderDate = useMemo(() => new Date(), [])

  ...
}

The point of this section is - BE AWARE of everything that you put into the component. Sometimes a harmless helper function defined in the component might just be the final straw on the performance.