DEV Community

loading...

Excalidraw state management

Eugene Karataev
undefined is not a function
・2 min read

Excalidraw is a nice minimalistic drawing tool for block diagrams, sketches e.t.c.

Excalidraw

It was written by Christopher Chedeau, who works at Facebook. He worked on projects like React Native, create-react-app, Prettier and many others.

The project uses React and Typescript and is open-source. I was interested what state management library top-notch frontend engineers use for their side projects nowdays.

Is it Redux? Redux Toolkit? MobX? Context API?

It turns out that no external state management library was used. Instead there is a custom mix of local component state and Redux.

I was interested in how this system works and I wrote a minimal example to reproduce Excalidraw's state management. There are three main blocks:

  • Actions. They are like Redux reducers: recieve state and an optional payload and produce a new state with changes.
export const increment = register({
  name: 'increment',
  perform: state => ({
    ...state,
    counter: state.counter + 1
  })
});

export const decrement = register({
  name: 'decrement',
  perform: state => ({
    ...state,
    counter: state.counter - 1
  })
});
Enter fullscreen mode Exit fullscreen mode
  • Action Manager. This guy is responsible for registering and performing actions.
export class ActionManager {

  actions: {[keyProp: string]: Action};
  updater: UpdaterFn;
  getState: GetStateFn;

  constructor(updater: UpdaterFn, getState: GetStateFn) {
    this.updater = updater;
    this.actions = {};
    this.getState = getState;
  }

  registerAction = (action: Action) => {
    this.actions[action.name] = action;
  };

  registerAll = (actions: Action[]) => {
    actions.forEach(action => this.registerAction(action));
  };

  renderAction = (name: string, payload?: any) => {
    const action = this.actions[name];
    if (!action) {
      console.log(`No action with name ${name}`);
      return;
    }
    const newState = action.perform(this.getState(), payload);
    this.updater(newState);
  }
}

Enter fullscreen mode Exit fullscreen mode
  • State. The application state lives in the root App component and gets updated from ActionManager.
const initialState: AppState = {
  counter: 1,
  todos: []
};

class App extends React.Component<any, AppState> {

  actionManager: ActionManager;

  constructor(props: any) {
    super(props);

    this.state = initialState;
    this.actionManager = new ActionManager(this.stateUpdater, this.getState);
    this.actionManager.registerAll(actions);
  }

  getState = () => this.state;

  stateUpdater = (newState: AppState) => {
    this.setState({...newState});
  };

  render() {
    return (
      <div>
        <Counter actionManager={this.actionManager} appState={this.state} />
        <hr />
        <Todo actionManager={this.actionManager} appState={this.state} />
      </div>
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

When application starts the app state is created and a new instance of ActionManager is instantiated. Both state and actionManager are provided as props to every react component down the tree. When a component wants to make a change, it calls actionManager.renderAction('someAction').

This is an interesting approach to state management which I did not met before. It has minimum boilerplate compared to the classic Redux.
There is props drilling with state and actionsManager, but it's not that bad.
The business logic is nicely grouped in actions folder and can be easily accessed from any component from the tree.

Here's the codesandbox demo if you're interested.

Discussion (1)

Collapse
sg1705 profile image
Saurabh Gupta

Thanks for explaining the state management in Excalidraw.

After reviewing the code in the sandbox, I couldn't understand why "ActionManager" has to be initialized inside a React component. Like global state, can it not be initialized outside of the component . Action manager doesn't hold state, it just transforms it.