Day 181: Swallowing the Redux Pill
Yesterday, I wrote an in-depth post explaining the challenges of sharing state between different contexts in a browser extension.
The conclusion there was that I’d forge on ahead with my fat models and custom hooks, but I decided to ask one of my closest friends what he thought just in case:

Given that he’s been a frontend wizard at Slack for way longer than I’ve been tinkering on my Chrome extensions, I very much trust him on these things.
Okay, so I’d give Redux a shot. Worst case scenario, I’d decide it’s not a good fit but come away with a lot more experience working with a way of building applications that’s celebrated as if it were the second coming of Christ.
So I added webext-redux and continued my migration to Redux. But then I saw something funny when testing things out:
Oh no.
I immediately knew what the culprit was — it’s the same one that causes keys to be dropped when typing quickly that I discussed in yesterday’s post. Because webext-redux sends a message to the background and waits for it to respond with the new state before updating its store, every update is asynchronous. Notice the stuff that needs to happen between the first step (Action) and the last step (Update).

Usually this isn’t an issue, but some tasks like typing into a form input or dragging and dropping need the state to update instantly — or else, you’ll render old state and experience glitches like in the video above.
I knew what I had to do to deal with the update taking too long. It’s the same solution I used for typing quickly into inputs from yesterday’s post: Add local state.
In this case, I’d need to create a local copy of the list of commands and update them when I got the response back from the background script. When I dispatch the “reorder commands” action, I’d need to reorder my local copy while waiting for the updated value to come back from the background script.
But this felt pretty whack. Every action I dispatch basically behaves like a network request, where I have to wait a bit before the state is updated. Sure, the response would be pretty fast most of the time, but the difference between “pretty fast” and “instant” is the difference between a glitchy interface and a responsive one. Surely, there must be a better approach than creating local copies of state and duplicating reducer functionality every time I need my UI to update instantly. Normally, people use their Redux store as their cache, but with asynchronous updates, I’d need a cache for my cache and that just feels a bit silly.
I’d already articulated what the solution would be in yesterday’s post:
The way to keep updates synchronous would be to give each context its own store and use message passing to manually keep them in sync, but that seems quite involved and I have zero interest in building that out.
“Zero interest”…
Heh, in my defense, that was before I decided to swallow the Redux pill.
Yup. Your boy built it.
If you’re surprised that I went from “I’m not going to use Redux” to “it’s time to build out my own middleware and higher-order reducers” literally overnight, you need to understand just how ridiculously annoying my inner engineering purist could be. Like I’ve said before, it’s sometimes great and sometimes hella inconvenient. Not quite sure which bucket today’s situation falls into.
Anyway, here’s a diagram of how it works:

Every new context (content scripts, popup, background script) creates a store, but unlike webext-redux (or electron-redux, a similar library for electron that uses the same design), those stores aren’t mere proxies. They act as the source of truth for that context, which means that state is updated immediately! 🎉
But if each context updates its own state, how do I make sure that the state is synchronized between contexts?
The answer comes from Redux’s documentation on the fundamental principles behind Redux:
The only way to change the state is to emit an action, an object describing what happened.
The implication: If I replay the actions performed in a particular context in another context, I get the same state.
Let’s look at an example:
- The background script and the popup both start off with state A
- The popup dispatches a
REORDER_COMMANDaction, which updates its state to state B - Now the states are out of sync: Popup has state B and background script has state A
But what would happen if I dispatched the same REORDER_COMMAND action in the background script’s store? Since we can use the same reducers, the background script will also now have state B! Thanks Redux!
Our problem is solved: State is updated synchronously AND shared state stays in sync between contexts.
The beautiful result:
Now, there is one very important failure case to note. Since there is a slight delay between a store dispatching an action and all the other stores dispatching the same action, it’s possible for the stores in other contexts to process actions in a different order.
Example:
- The background script dispatches a
DELETE_COMMANDaction - At the exact same time, the popup script dispatches a
REORDER_COMMANDaction - The popup receives the
DELETE_COMMANDaction from the background script - The background script receives the
REORDER_COMMANDaction from the popup
At this point, the background script’s action order is (1) DELETE_COMMAND and (2) REORDER_COMMAND whereas the popup script’s action order is (1) REORDER_COMMAND and (2) DELETE_COMMAND.
You can see why that might be problematic. If DELETE_COMMAND removed a command based on its order in the list, the background script and the popup would end up deleting different commands!
Obviously, having shared state go out-of-sync is serious bad news bears. The plan that I have for handling this is to reset the state with the background’s state when a conflict is detected. In essence, this would treat all changes from non-background contexts like optimistic updates and use the background script’s store as the ultimate source of truth. This is something I need to test more fully, and it’s possible that I discover that this won’t work for some reason and that this approach is more trouble than it’s worth. But so far it’s been pretty promising, and I have my synchronous updates back!