useEffect race ate the layout of every freshly-created startup
New startups appeared in the sidebar fine, but their pane layout disappeared on the next launch. Two effects (Load and Save) were racing on the initial render, and the Load effect's stale closure was overwriting the Save.
Symptom
- Create a new startup (e.g.
photo-ai). - Open a pane, see it render.
- Restart the app.
- The startup is still in the sidebar — but its layout slot is empty, the pane is gone.
Existing startups persisted correctly. Only newly created startups lost their layout.
Cause
The active-startup-id state had two effects bound to it:
- Load effect — when active id changes, read layout for that id from localStorage and set it in component state.
- Save effect — when layout in component state changes, write it back to localStorage under the active id.
On a brand new startup, both effects fired on the same tick. The Load effect captured an empty initial layout in its closure. Once the Save effect wrote the user's real layout, the Load effect's stale closure overwrote it back to empty before the storage round-trip settled.
Fix
Three layers of defence:
- Synchronously seed the layout when the startup is created, so the Load effect never reads "nothing for this id".
- Synchronously save inside the Load effect when it finds nothing — pin a known baseline before the Save effect runs.
- Null-guard the Save effect so it never writes
nullover a real value.
Commit: 51cac0a. Documented as a six-section post-mortem in docs/ISSUES.md.
Pattern
Two effects keyed off the same state, one reading and one writing the same store, will race on the first render of a new entity. The cleanest fix is to remove the race outright (synchronous seed at creation time), not to choreograph the effect order. If you find yourself reasoning about effect ordering, the data model is already wrong.