Where is our store stored?

Guillaume Wuip
BlaBlaCar
Published in
7 min readNov 24, 2021

The BlaBlaCar teams in charge of our user-facing web application recently started a big architecture initiative: it’s time to rely a little bit less on React, leaving behind Redux and the files structure we put in place a few years ago for our Single Page Application (SPA).

We’re designing the codebase we need to scale and support both the application’s features and the teams. We want to enforce the separation between features by removing the global shared state in order to reduce coupling and improve ownership. We also want business logic to be written outside React to stop mixing domain definitions with view lifecycles.

The “why and how?” is a (long) story for another blog post. What interests us today is what we have learned about my favorite frontend topic, the “State/Store dilemma”, because of this project.

What is this “ State/Store dilemma”, you may ask? In every frontend application, data (the State) is stored somewhere (the Store) and this duality shapes how we design our code. I’m always looking at technical issues through this lens — current and previous colleagues know this all too well (sorry colleagues!).

There is now this new “Store question” I ask myself again and again around this dilemma: Where is the store stored?

Where is the store stored?

The reality of the store that lives “somewhere” in the code is the very problem. The store in itself, whether a simple variable or a full Redux store, has to be stored somewhere in our browser, NodeJS or other fancy Javascript engine runtime.

Deciding where our stores are stored is not trivial and comes with big impacts. Let me draw here the three main potential locations we can use for our stores: modules, libraries or frameworks locations and classic programmatic stores.

Modules

We can use modules to host our stores. It’s quite convenient and it makes full use of the Javascript ES6 language.

Let’s say we have a module storing Cars (our state here).

// cars/store.js
const _cars = []
export function add(car) {
_cars.push(car);
}
export function list() {
return _cars;
}

This cars/store.js module acts as a very naive store with both read (list) and write (add) functionalities.

Using this store in the rest of the codebase is very easy: we just have to use classic ES6 imports.

// another/part/of/the/codebase.js
import * as Cars from '../path/to/cars/store';
function doingSomething() {
console.log('current cars: ', Cars.list());

const newCar = "My old car";
cars.add(newCar);
}

And voilà! Because ES6 modules are stateful, the _cars “private” variable will stay the same across all imports, making cars/store.js a store by itself.

Should we need a reactive store — ie. being able to subscribe to state changes, we would write our own subscribe function by hand or use a library for that.

Libraries / Frameworks location

Another very common way of storing our store is using the locations provided by the main view library or framework we use (eg. React, Vue, etc.).

Let’s say that we’re using React here. The go to store location is then a simple useState hook.

// app.js
import React from 'react';
import { UserContext } from './cars.js'function App({ children }) {
const [cars, setCars] = React.useState([]);
const addCar = car => setCars(cars => [...cars, car]); return (
<MyComponent cars={cars} addCar={addCar} />
)
}

We’re not using modules to host our store anymore. React does it for us via useState. Note that we’re reactive by default here: calling addCar in MyComponent will rerender all <App />.

Using useState alone usually doesn’t scale well as we’re forced to pass cars and setCar everywhere through props. This issue is called props drilling. To overcome this, one can add a Context to make our state and its API are available to every child component — even if it’s not made for that. The idea stays the same: the store is stored inside React “runtime” via useState.

Programmatic store

And finally, there’s the good old way: dependency injection by hand. Let’s say we’re in a full custom app (no React shenanigans) and we don’t want to use modules. We can then craft a store instance when the app starts and pass it along everywhere via function parameters.

Let’s write a store factory very similar to the ES6 module option.

// store.js
function buildStore() {
const _cars = [];
return {
add: (car) => {
_cars.push(car);
},
list: () => {
return _cars;
},
}
}

Now that our store’s factory is ready, we just have to craft a store instance somewhere in our app’s entry point and pass it along everywhere.

// index.js
function startApp(store) {
store.add('new car');

document.body.innerText = store.list().join(', ');
// ...
}
function initApp() {
const store = buildStore();
startApp(store);
}
initApp();

Of course, any function that needs to access the store will have to receive it via its parameters… We’re right back where we left our props drilling issue.

React/Redux location in our SPA

As we’re in the process of removing Redux and relying a little bit less on React, we can’t deal with another “library/framework location”. Why?

The big advantage of the current situation is probably the strong integration between React and Redux thanks to bindings like react-redux : it’s like the Redux store is stored “in React runtime”. We just have to craft it before rendering our page then store it inside react-redux’s Provider. In doing so it will be scoped to the ReactDOM.render or ReactDOMServer.renderToString (which will be called once for every http request).

The main issue with that is however that we’re locked to the corresponding library / framework. We’re moving towards an SPA where we want to limit React to the views, and where we need to read / update / subscribe to store changes in other contexts. With a “library/framework location”, we just can’t, it’s React or bust. Back at square one.

ES6 module location?

What about the ES6 modules option? Pros: it’s raw ES6 (import / export) and it is quite simple to grasp. It’s also very flexible. We can do more or less whatever we want in a module and every part of our codebase can use it.

But there is a big constraint. Our codebase is used in different runtimes: it runs in the browser but also in NodeJS for server-side rendering (SSR). In the browser, we can make the assumption that there is only one user at a given time using the runtime. But on NodeJS our module state will be shared between multiple HTTP requests, so multiple users. Boom! Enjoy the data leak issue.

We don’t have threads or the luxury of a single runtime per user with NodeJS. One can find solutions exploring low level NodeJS primitives or V8 deep features to isolate runtimes, but it’s certainly not out of the box and we chose not to investigate this for now.

Programmatic stores like in the good old days

Let’s give another chance to our programmatic store sharing solution then.

It’s clearly not very convenient to have to pass our store programmatically to every part of the codebase as it creates a lot of boilerplate, but at least we face no issue with SSR if we craft one store per request server-side, and one store when the app starts browser-side. It is very flexible as well: we can do whatever we want with our store and use it everywhere in the codebase (as soon as it’s injected).

It is a good low-level start on top of which we can build abstractions for the different parts of the codebase. What we’re doing at BlaBlacar is crafting our stores out of React, then providing React contexts for the views and automatic dependency injection for other parts of the code.

Let’s say we’re dealing with a feature flags-related store. We would first craft this featureFlags object then pass it along to the rest of the code base, whether it’s non-React or React-related stuff.

// featureFlags.js
function buildFeatureFlags() { … }
// index.js
const featureFlags = buildFeatureFlags()
const anotherLowLevelModule = buildMyLowLevelModule(featureFlags)renderApp(featureFlags)

Here, the renderApp would place featureFlags in a React Context for components to be able to access it via a React hook.

function MyView() {
const featureFlags = useFeatureFlags()
if (featureFlags.isFeatureActivated()) {
return <VersionA />
} else {
return <VersionB />
}
}

Boom! Good developer experience when writing components while assuring us the flexibility we need.

No one location to rule them all

There is no perfect solution to host stores with Javascript. It’s all about compromises. It’s not because we choose to go for the programmatic option at BlaBlaCar that it’s the option that will best suit you.

So, every time we deal with a new store, let’s also give a look at where the store is stored to understand the constraints that come with its location.

Using ES6 modules is very handy, but we should be careful about potential data leaks in server-side rendering contexts.

For libraries and framework locations, we should keep in mind that the decision not only impacts the store locations themselves, but the whole application architecture as we will be unable to access stores outside of the corresponding library/framework.

When all those compromises are not acceptable for us, we’re left with the classic programmatic store approach that gives us more flexibility but also more boilerplate.

Another fascinating part of the State and Store puzzle!

Special thanks to Thomas Salandre, Thomas Pocreau, Antoine Sauray and Pierre Thirouin for their help in reviewing drafts of this blog post! ❤️ It is an adapted version of a previous, more generic one.

Are you interested in working on such frontend topics at BlaBlaCar? We’re hiring! Let’s get in touch :)

--

--