Zeppelin Labs says
Published in

Zeppelin Labs says

Building state management solutions for Creative Tools' Editors

Intro

In this article, we will share some of our (mildly opinionated) insights into designing and building scalable state management solutions in the context of creative tools editors with React, Immer and Recoil as foundations of our approach.

To better illustrate our approach, we will include some code examples and a small prototype.

We have been using this knowledge in a couple of different projects, allowing us to easily escape some of the most common pitfalls.

App state vs design state

One of the main distinctions we can make in these kinds of apps is App state vs Design state.

Design state

Describes the user-created content, for instance: text (font, size, color, content), images, grouped elements, connected elements, etc.

The design state changes each time the design conceptually changes, but not when the user does actions that don’t impact the design.

Some examples of actions that impact the design state:

  • Move an element
  • Change properties of a text (color, size, font)
  • Group elements

App state

Defines the current app state, excluding the design. The app state changes when the user interacts with the app and they menus.

Some examples:

  • Select an element
  • Open a dialog to edit the properties of an element
  • Show the right-click menu
  • Confirm an action

Why is this distinction important?

Each kind of state has they own particularities, that doesn’t apply to the other, and would cause our implementation to be more complex if kept together.

For example:

  • When saving a design, only this part of the state is saved.
  • Undo/redo only applies to the design state. The app must be able to work under small inconsistencies between the design and the app state, for example, when the selected element is no longer in the design because of an undo.
  • When loading a design, the design state is simply set with the design, while the app state is mainly initialized with a default value.

Implementation considerations

App state

Whenever we can, we must favor local state over global state, the app state is no exception.

Analyzing code and its flow becomes easier when we use local state, components must explicitly pass data between one another and their interactions are explicit, there are no “faraway hidden” use effects that we have to manually look for.

Design state

When defining the design state, we must have the following considerations:

  • Be able to get and set the whole state so that we can easily implement:
    – Loading and saving a design
    – Undo/redo
  • Avoid rendering all the elements when only some of them change

How can we implement undo/redo easily?

One of the easiest ways to implement undo/redo in a React application is to use Immer to generate modification patches.

Immer is a JS library that allows us to write data modifications in an immutable context (for example React state) in a “mutable way”.

Another feature of Immer is the generation of do and undo patches of a modification, this allows us to save how to re-execute the change we made and how to go back to the previews state.

After each state change, we should save the do and undo patches, and use them when the user triggers the undo/redo.

Why do we need to get and set the whole state at the same time?

The alternative is not having it together, this doesn’t seem like a big problem for loading and saving, we just have to get/set all the different state parts.

But it becomes a problem when we have to implement undo/redo. For each state part, we have to save the produced patches, with metadata indicating which state part it is for, and read it when an undo is triggered so that we can modify the correct state part.

Also, because a user action can modify multiple state parts as one operation, we have to keep track of which patches belong to the same operation so that we can undo and redo them at the same time.

Using a single state would solve all these things

  • There are no actions that modify multiple states
  • All the patches apply to the state, not to multiple distributed states.

Get and set the whole state

The easier way to satisfy this design choice is to save the whole design state in the same place. That would get us some think like useState<DesignState>(defaultState), as you probably guess, this causes us to fail our “render most of the app” consideration:

Not rendering most of the app when the design changes

To solve this, we generally use Recoil, a state management library for React.

Recoil has two main concepts: atoms and selectors.

Atoms: unit of state, that can be used in a similar fashion to useState, but globally, for example

As the useState implementation, in the code above, all the DesignElements will render whenever any element (or any part of the state) changes. To solve this, we can use selectors.

Selectors: functions that create a projection from an atom or other selector. When a React component uses a selector, it will re-render only (with some caveats) when the result of the selector changes.

Recoil also allows us to receive arguments in the function that defines the selector, those kinds of selectors are called selectorFamily. We can use this to create a selectorFamily that receives an elementId and gives us the element.

When an element is modified, the above code only triggers an update to the matching DesignElement and doesn’t render all the elements or the Design component.

An attentive observer may view that the Design component will render whenever a component is added or deleted, triggering the re-render of all the DesignElements. If this causes performance problems for a particular use case, we can wrap our DesignElement component in a React.memo.

Set the state

Because we want our sets to be applied to the top-level DesignState (to simplify undo/redo), we can create recoil callbacks to encapsulate our modification logic and undo/redo patches creation.

A minimal example can be found in this Codesanbox.

Closing

In this blog post, we shared what are the main drivers when designing state management in the context of creative tools' editors, and our go-to implementation that satisfies them.

If you read it all the way here I would love to know your opinion. You can reach out to me here.

At Zeppelin Labs, we help founders and growing companies experiment and build differentiated digital products that drive growth. You can find us here. Or here. Or here.

If you would like to join our team, email us at jobs@zeppelinlabs.io

Would like to partner? email us at partnerships@zeppelinlabs.io

Subscribe to our Newsletter here.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store