loading...

Excalidraw state management

karataev profile image Eugene Karataev ・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
  })
});
  • 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);
  }
}

  • 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>
    )
  }
}

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.

Posted on Mar 20 by:

Discussion

markdown guide