Excalidraw is a nice minimalistic drawing tool for block diagrams, sketches e.t.c.
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 fromActionManager
.
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.
Top comments (1)
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.