Our journey
Last updated
Was this helpful?
Last updated
Was this helpful?
Initially the state management of the CodeSandbox client was written in Redux. This was at a time where it was considered the go to solution for any complex state management in React applications. Redux server the client well as it allowed sharing of state between many components, which is a very common scenario.
The challenge with Redux is that there is no strong concept of how to express asynchronous logic and other side effects. This results in a mix of redux thunks and logic split across many components. It was impossible to infer looking at the code what actually happens when a sandbox is opened.
Cerebral with its signals concept was an ideal solution for the primary challenge of the codebase, which was to co locate logic and increase the understanding of what code runs when you open a sandbox and other functionality. The Cerebral devtools was also a huge benefit.
The following presentation gives you an overview of the refactor and what Cerebral is about.
And the following article goes in depth of the Redux => Cerebral rewrite.
When TypeScript came out as the leading solution over Flow we wanted to take advantage of type safety in CodeSandbox. Adding new features and fixing bugs often led to more bugs. The reason is that there was really no way for us to know what components were affected by changes to the state or logic in the Cerebral sequences. The big problem though is that Cerebral did not really have good support for TypeScript.
As proxies became standard in modern browsers we gained a new super power that could be used to improve state management. As mentioned in the article above we could express our logic in a mutable manner , gaining automatic tracking of accessed state in components and still prevent "out of control" mutations. The successor of Cerebral was named Overmind and as it matured CodeSandbox would take benefit of its TypeScript support. The functional sequence API was still available as operators, but we had the freedom to do simple straight forward imperative actions as well.
Moving to TypeScript had a huge benefit for us. We could now confidently refactor and add new features. Of course there were many annoying situations with TypeScript, for example with our automatic typing coming from our GraphQL endpoints. That said, the overall experience is overwhelmingly good. There was one feature of TypeScript we were not able to take advantage of though, strict null checking. There is a lot of state values in the initial client that might or might not be there, where many components and actions would just assume that the values were present. But have a strict null check compatible codebase is not just switching the option in your TypeScript config file, the code was expressed in a way that made way too many assumptions and a refactor would just be too much work. With the way we expressed our code we would need literarily hundreds, if not thousands, of IF statements in our codebase to see if state values where actually present.
An other challenge we had in our codebase was dealing with asynchronous logic. The initial client has a lot of async/await and try/catch. Events coming from the environment, it being user interactions, responses from requests etc. would bluntly change the state of the app, even though intermediate actions had completely changed the context of the app. An example of this would be opening a workspace and then as it loads change to a different workspace. The first workspace results would populate the UI. There was no typing on the errors either, which meant we did a bad job acting on specific errors. There was no good mechanism for unsubscribing to data as the user moved through the app, as it was tied to imperative calls in actions, instead of being reactive to components unmounting.
As we were exploring the next steps of CodeSandbox we had to make a choice. Continue with the existing client or build a new. With the overwhelming complexity of doing a refactor to support strict null checking and no obvious way to fix our other issues, we decided to take a fresh look at the current state of React and what the ecosystem had to offer in terms of solving our issues.
At this point in time React had matured a lot with its hooks and context providers. It had a solid concept for splitting up logic and UI, where new exciting features like suspense could also be taken advantage of. But more importantly the concept of state machines was starting to take a foothold. What is not obvious is that they actually solve every single issue we had with the current codebase, we just needed to find a way to use them with React. The most prominent library for state machines is XState, but it was built before TypeScript was a thing and it is agnostic to the view layer. It also has a ton of features, academic terminology and concepts that felt unnecessarily complex. Through many iterations we tried to imagine how a state machine concept would look like as a first class concept in React.
The final iteration of this concept came from looking at the existing state management hooks of React, useState and useReducer, and imagine what the next natural hook would be. This new hook at its core is a useReducer using explicit states. Explicit states is the foundation of what solves all our challenges.
Strict null checking: With explicit states, used by state machines, there is no need to check individual state values being present or not. There are no string | null
types of typing. We match
on the explicit state and any related values are typed as being present
Async state changes: When actions hit the reducer they are only handled if the action is valid for the current state. This ensures that no unwanted state changes are made related to asynchronous logic
Async side effects: All side effects are fired related to state transitions, meaning that an action can not imperatively run any side effect. Since the reducer only handles actions valid for the current state, the side effects are also triggered only in valid states
Unsubscribing: Since our side effects are now being run inside React hooks we have a natural way to trigger unsubscribe. When components unmount
Typed errors: As an addition to the new state management hook you can create subscriptions. That means we are not using promise based APIs within the context of React at all, we only subscribe to actions that are dispatched from the environment. That means we can have multiple error actions which are typed and dealt with separately inside the reducer
The CodeSandbox architecture using react-states adds additional patterns on top of this core functionality to create an explicit concept of features.