A store created at module scope works in the browser but leaks data between users when the tree is rendered on a server; why?
answer
- module scope means one per process
- a browser process is one person
- a server process is many requests
- factory, not instance; provide per render
basics
~20 sA store created at module scope is one instance per process. A browser process serves one person, so that is what the author wanted; a server process renders many requests at once, and all of them share that instance. Create one store per request instead.
solid answer
~50 sState created at the top level of a module is created once per process, the first time the module loads, and lives as long as the process. In a browser that process belongs to one document and one person, so process-wide and user-wide coincide and the pattern looks proven. A server process handles many requests, often interleaved: every request reaches the same instance, so one request's write is visible to another's render, and whatever the previous request left behind becomes the next one's starting state. Clearing the store at the start of a request makes it worse — it wipes a concurrent request's in-flight data. The fix is to export a **factory**, not an instance: create one store per request, provide it at the root of that render, and have components read it from an ancestor rather than importing it. Tests get the same seam and stop depending on order.
go deeper
Recall that state created at the top level of a module exists once per process. In a browser that is one person; on a server it is every request that process handles.
Explain the three concrete consequences on a server — bleed between concurrent requests, state persisting into the next request, and interleaved writes — and why a per-request clear does not fix any of them.
Treat it as an exposure bug and show how you catch it: two differing requests rendered in one process, the same request rendered twice, randomised and parallel test runs, and an audit of module-scope state.
Make the rule structural rather than remembered: stores are created per request and provided, factories are the only export, and review rejects a module-level instance unless it demonstrably carries nothing about a person.
## What module scope actually means A value created at the top level of a module is created once — the first time that module is loaded into a process — and lives as long as the process does. Not per user, not per page, not per test: per **process**. Everything about the singleton problem follows from that sentence, plus the fact that a browser and a server mean very different things by the word. ## Why the browser hides the problem In a browser the code runs in a process belonging to one document, driving one screen for one person. Process-wide and user-wide are the same thing there, so a module-level store does exactly what its author wanted: one instance, reachable from anywhere, alive for the session. A client-side navigation does not reload modules, so the store survives route changes too — usually desirable, and the reason the pattern feels safe. ## What a server does with the same code A server process handles **many requests, often concurrently and interleaved**. The module loads once; every request rendering the tree reaches the same store. The consequences, roughly in the order teams discover them: - **Bleed between users.** One request writes the signed-in person into the store; another request rendering at the same moment reads it and renders someone else's name. This is the failure that matters: a data-exposure bug, not a cosmetic one. - **Persistence across requests.** Whatever the previous request left behind is the starting state of the next, so a fresh visitor's first render can show stale data. - **Interleaving.** Because a render can pause and resume, two requests' writes alternate inside one store, producing states neither request intended. - **A clear at the start of a request makes it worse.** It looks like isolation and is really a write that wipes a concurrent request's in-flight data. ## Tests have the same shape A test file importing a module-level store shares it with every other test in that process. The symptoms are familiar: a case that passes alone and fails in the suite, an order-dependent failure, a value leaking from one case into the next. A reset between tests is the same partial fix as clearing per request — it holds while tests run strictly one at a time in one process, and stops holding the moment the runner parallelises or reuses workers. ## The fix: one instance per unit of work Stop exporting an instance. Export a **factory** that creates one. | Packaging | Instances | Isolation | Where it is right | |---|---|---|---| | module-level instance | one per process | none | a browser-only app with no server rendering | | factory, one per request | one per request | complete | any tree rendered on a server | | factory, one per test | one per test | complete | every test suite | The tree then needs a way to find the instance belonging to its own render. That is the ancestor-provides-a-value channel: create the store where the request is handled, provide it at the root of that render, and have components read it from their ancestors instead of importing it. The same seam hands every test a store it owns, which is usually the point at which the tests get shorter. ## Handing state to the client If the server rendered markup from a store, the browser's first render has to agree with that markup, so the data the server used travels with the response and seeds the client's own instance. The client then creates its store — one per document, the case where per-process genuinely is per person — and carries on from there. ## How to detect it before a user does 1. Render two different requests in one process, with different data, and assert each output separately. A shared store fails this immediately. 2. Render the same request twice in one process and assert the second output equals the first. A store that accumulated state fails here. 3. Run the suite with ordering randomised, and again with more than one worker. 4. List every piece of state created at module scope and ask of each: would this be correct if two people's renders were in flight at the same time? ## The honest exception A module-level instance is fine for state that genuinely is per process and carries nothing about a person: a registry of component or feature definitions, parsed configuration, a counter used only to generate local identifiers. The test is not "is it convenient to import" but "would two concurrent renders for two different people both be correct sharing this". When the answer is no, it belongs to the request.
- How does the tree get a per-request store without importing a module-level one?Create it where the request is handled, then provide it at the root of that render so descendants read it from an ancestor instead of importing it. Components take the store from that channel, which means a test can hand in its own instance through the same seam with no module patching.
- What has to travel to the client alongside server-rendered markup?The data the server's store held when it rendered. The browser creates its own instance and seeds it with that data, so the first client render produces the same output as the delivered markup. Without it the first render disagrees with what the user is already looking at.
- Is a reset between tests an acceptable substitute for a fresh instance?Only while tests run strictly one at a time in one process. It breaks as soon as the runner parallelises or reuses workers, and it silently depends on every test remembering to call it. A factory gives each test an instance it owns, which needs no discipline to stay correct.
A module-level store is one whiteboard in a shared office: perfect while you are the only occupant, disastrous when a hundred visitors an hour write on it at once.
saying these in an interview costs you the question
- Thinks one instance per process means one instance per user.
- Clears the store at the start of each request and calls it isolated.
- Assumes each server request gets its own copy of module state.
- Only ever runs tests sequentially, so the bleed never appears.
- Treats cross-request data bleed as a caching quirk, not exposure.