Surprising Performance Lessons from React Microfrontends in Production
Tags: react, performance, microservices, svelte
Published: 2022-05-22 12:35:48

The epilot engineering team stands at 27 developers 1 year after the launch of our rewritten portal built on mostly* React [microfrontends](https://docs.epilot.io/docs/portal/microfrontends).

[![Microfrontends screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aym8yh77ndlw26rfv0nn.png)](https://docs.epilot.io/docs/portal/microfrontends)

_*Part of our app is written using other frontend frameworks, most notably the sidebar navigation written in Svelte._

Since the initial launch a year ago, our teams have gained a lot of experience running React microfrontends in production using [single-spa](https://single-spa.js.org/).

While we expected to face challenges with our new frontend microservices architecture, after solving a few initial problems we haven't hit any major snags with single-spa in the first year.

To my surprise, most issues that have emerged in our codebase are general React pain points not specific to microfrontend architecture at all.

In an effort to share knowledge, I'll address the most common React performance issue we've seen reemerge in our teams in this post.

## The state management problem

Here's a really common hook pattern I've seen emerge at one point in most of our React microfrontend projects:

```jsx
// useFormState.jsx
import React from 'react'

const FormContext = React.createContext()

export const GlobalFormStateProvider = (props) => {
  const [formState, setFormState] = React.useState({})

  return (
    <FormContext.Provider value={{ formState, setFormState }}>
      {props.children}
    </FormContext.Provider>
  )
}

export const useFormState = () => React.useContext(FormContext)
```

```jsx
// App.jsx
import { GlobalFormStateProvider } from './useFormState'
import { Form } from './Form' 

export const App = () => (
  <GlobalFormStateProvider>
    <Form />
  </GlobalFormStateProvider>
}
```

```jsx
// Form.jsx
import React from 'react'
import { useFormState } from './useFormState'
import { api } from './api'

export const Form = () => (
  const { formState } = useFormState() 
  
  const handleSubmit = React.useCallback(
    () => api.post('/v1/submit', formState),
    [formState]
  )

  return (
    <form onSubmit={handleSubmit}>
      <FirstFormGroup />
      <SecondFormGroup />
    </form>
  )
)

const FirstFormGroup = () => (
  const { formState, setFormState } = useFormState()

  return (
    <div className="form-group">
      <input
        value={formState.field1}
        onChange={(e) => 
          setFormState({ ...formState, field1: e.target.value })}
      />
      <input
        value={formState.field2}
        onChange={(e) => 
          setFormState({ ...formState, field2: e.target.value })}
      />
    </div>
  )
)

const SecondFormGroup = () => (
  const { formState, setFormState } = useFormState()

   return (
    <div className="form-group">
      <input
        value={formState.field3}
        onChange={(e) => 
          setFormState({ ...formState, field3: e.target.value })}
      />
    </div>
  )
)
```

Many readers will immediately recognize antipatterns in the above example, but entertain the naïve perspective:

The `useFormState()` hook is very useful. No prop drilling. No fancy global state management libraries needed. Just native `React.useState()` shared in a global Context.

What's not to love here?

## Perf issues

As nice as `useFormState()` seems, we'd quickly face performance issues due to components using it having to render on every `setFormState()` causing unnecessary, potentially expensive re-renders.

This is because we've subscribed all our Form components to re-render on all changes in `FormContext` by using `React.useContext(FormContext)` inside `useFormState()`.

You might think `React.memo` to the rescue, but reading the [React docs](https://reactjs.org/docs/hooks-reference.html#usecontext):

> When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a re-render with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a re-render will still happen starting at the component itself using useContext.

Further, we're unnecessarily depending on the full `formState` object in all our form components.

Consider:

```js
// formState is a dependency:
setFormState({ ...formState, field1: e.target.value })}
// formState not a dependency:
setFormState((formState) => ({ ...formState, field1: e.target.value }))
```

At this time, I would consider Context Providers using `React.useState` to store complex global app state a general React performance antipattern.

However, if React adds `useContextSelector` ([RFC](https://github.com/reactjs/rfcs/pull/119)) I am positive the situation could change. 🤞

## Lessons learned

Seeing antipatterns like these emerge in React projects even with fairly experienced frontend developers (think 5+ years of React) has lead me to consider performance as a topic that unfortunately requires pretty significant investment to produce quality output when working with React in general.

As always, there is [No Silver Bullet](https://en.wikipedia.org/wiki/No_Silver_Bullet). However, our frontend microservices architecture has enabled us to cheaply experiment with different approaches in different teams who have produced quite a few competing strategies to solve form performance:

- Use of global state management libraries e.g. [Redux](https://redux.js.org/), [MobX](https://mobx.js.org/README.html) and [XState](https://xstate.js.org/).
- Use of dedicated form libraries e.g. [react-hook-form](https://react-hook-form.com/)
- Use of this implementation of [useContextSelector](https://github.com/dai-shi/use-context-selector)
- Avoiding controlled form inputs (Leverage the web platform! 👐)

Additionally, thanks to the flexibility of [single-spa](https://single-spa.js.org/) we've been able to experiment outside of the React ecosystem with frameworks like [Svelte](https://svelte.dev/) and others which has been extremely promising and rewarding for our engineers.

[![epilot logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q660re5susj1a8efggrd.png)](https://epilot.cloud/en)

[We're hiring @ epilot!](https://promise.epilot.cloud/)