Do you want a sweet state too?

React-sweet-state: Redux and Context, the yummy parts

Alberto Gasparin
Jun 18 · 5 min read

For the past months I’ve been part of the team responsible to transform Atlassian’s Jira into a modern SPA. We took this opportunity to experiment with new concepts, especially around state management.

Our first macro component to be built was the navigation app. A navigation app poses quite a few challenges, because of the way other apps have to interact with it. We were not much concerned about data fetching, which would eventually be solved by React Apollo, but more focused on how we could share generic data (like UI state) and trigger cross-app actions.

Why current solutions didn’t taste good enough

We evaluated and implemented several different technical solutions, however neither was flexible enough for our use case:

  • Redux: the de-facto standard for state management has a big limitation: cross provider communication could be challenging. Given every app in Jira had a Redux provider at the top (for its own state), it couldn’t talk with the navigation Redux store on another part of the tree. Moreover, transitioning to a single Redux store for the entire Jira seemed like a herculean effort, definitely not deliverable in a few months time (if ever) and code-splitting a shared store poses other challenges.
  • React Context: offers a simple way to communicate across components, but without an abstraction layer around it, it turned out to be hard to implement a solid state logic while worrying about both the Consumer and Provider components lifecycles and even harder to debug. Another issue we found is scalability: every piece of data we wanted to share needed a different Provider (given the lack of selective re-renders in the Consumer side). Also, Providers had to be put at the top of the tree to ensure data availability, or defaults has to be implemented which might result in code duplication.
  • Other solutions offered interesting takes on solving this problem but the uncertainty about their future and support raised concerns about adoption within Jira. We needed something that worked at Jira’s scale.

Avoiding the bad parts and focusing on the yummy ones

While trying some of those implementations, we listed of what we liked about Redux and React Context and started thinking how to build a solution that would take advantage of their strengths while avoiding the downsides.

For instance, our Redux good parts list contains:

  • Separation of concerns: business logic is encapsulated in actions/reducers, not in the “view”/render of a react component
  • Selectors and mapStateToProps: components can get only a slice of the state and update only when that slice changes
  • Testability: actions/reducers are decoupled from lifecycle methods
  • Performance
  • Dev experience: dev tools, undo/redo ability
  • SSR support
  • Middlewares

And our React Context good parts list contains:

  • Composability: components share state, no need to “connect”
  • Nesting ability: Providers/Consumers can be nested and still able to communicate/share state
  • State can be “local”, no need for it to be always global and shared across the entire app
  • Could work even without a Provider at the top
  • Less boilerplate
  • Easy code-splitting

By combining all the above we came up with .

React-sweet-state 101

Sweet-state has four main concepts: Stores, Actions, Subscribers/Hooks and Containers.

What are Stores and Actions?

A Store is mostly like a Redux “duck”, an object with initialState and actions:

import { createStore } from 'react-sweet-state';const initialState = { count: 0 };const actions = {
increment: () => ({ getState, setState }) => {
const { count } = getState();
setState({ count: count + 1 });
}
};
const Store = createStore({ initialState, actions });

Actions mutate the Store state synchronously (thus they can be async) and to access a Store state we create dedicated Subscribers or Hooks.

How to access a Store state?

exports utilities to create render props components (called subscribers) or custom hooks to access the state, mutate it via actions and re-render on change.

import { createSubscriber, createHook } from 'react-sweet-state';
import Store from './my-store';
export const CounterSubscriber = createSubscriber(Store);
// or
export const useCounter = createHook(Store);

Then your components will just consume the above:

import { CounterSubscriber, useCounter } from './counter-state';function MyComponent () {
return (
<CounterSubscriber>
{(state, actions) => /* ... */}
</CounterSubscriber>
)
}
// orfunction MyComponent () {
const [state, actions] = useCounter();
return /* ... */
}

The example above is ridiculously simple, but Jira is not. That is why we implemented Sweet-state to handle scale while remaining performant and manageable. On top of the above API, Sweet-state supports:

  • Container components: they allow a specific Store to have local instances, or global instances with dynamic keys, without requiring changes to Subscribers, hooks or Stores (thanks to React Context)
  • It supports selecting values and re-rendering only when the resulted value changes (same as Redux selectors), without using shouldComponentUpdate or PureComponent though.
  • It has been developed with performance in mind, avoiding useless re-renders and overcoming some of Context API performance limitations.

Like sugar on top of Context API

It has a base of React Context, a simple yet performant pub/sub mechanism, React like API to handle state changes and the best practices to enjoy out-of-the-box support for React goodies (like Suspense and Hooks). Indeed, uses a default Context to hold global Store instances into a single “registry”. Subscribers/Hooks act like a Context Consumer, except they do not use the Consumer component directly in order to achieve greater flexibility and performance. Finally, Containers are basically Provider-Consumers all in one, retaining the ability to look for instances in the global registry while allowing also a local one. The main difference with plain Context is that protects you from common mistakes, ensuring performance and good debuggability.

Looking for a sweet spot

Despite the initial skepticism of other devs (even within Atlassian), is growing in popularity and it is now the de-facto solution for cross app state sharing in Jira. If you are facing similar problems, like trying to decouple your giant Redux store into smaller pieces, maintaining performance and your dev sanity, .


The opinions expressed are my own and don’t necessarily reflect those of Atlassian.

Alberto Gasparin

Written by

Being a Frontend Developer is learning something new every day