DEV Community

linou518
linou518

Posted on

When a Saved Task Disappears After Refresh: Fixing a Dual Data Source Trap in a SPA

When a Saved Task Disappears After Refresh: Fixing a Dual Data Source Trap in a SPA

While reviewing a dashboard’s project task screen, we ran into a classic frontend trap. The symptom looked simple: after adding a task, it immediately appeared in the UI, but after a page reload it vanished. The first suspects should usually be an API failure or a broken save path. This time, neither was the root cause. The real issue was worse: two different implementations were pretending to be the same feature.

There were actually two data flows in the frontend. One path lived in app.js. It loaded tasks.json through loadData() and sent add, delete, and toggle operations to /api/task/add, /api/task/delete, and /api/task/toggle. That path went through the backend, so the data was persisted. The other path lived inside an inline script in index.html, where it directly mutated an in-memory object called simpleProjectsData. On screen, both paths looked like “a task was added.” In reality, the second path was only changing temporary browser state, so everything disappeared after refresh.

That is what makes this kind of bug annoying: the UI looks alive enough to fool you. The button responds. The list updates. So the eye goes to rendering first, not to persistence. But the real problem was architectural. The moment you have two competing sources of truth, you have already lost the design battle. One path trusted tasks.json. The other trusted page memory. It was only a matter of time before they diverged.

The fix was not dramatic. First, we updated /api/task/add so it could accept task as well as title, making it easier for the UI to call the backend path consistently. Next, we added /api/task/delete so deletion would remove the matching line from Markdown, run _regenerate(), and rebuild tasks.json. In other words, the goal was not to make the screen look updated. The goal was to force all writes through a single persistence path.

The lesson was clear: in SPA debugging, it is often faster to question ownership of state than to stare at the visible symptom. Especially in long-lived single-page apps, temporary scripts and old implementations tend to survive. Over time they start sharing responsibility for the same feature through different routes. At that point, the real fix is rarely another if statement. It is deciding what the single source of truth should be, and removing the rest.

Frontend work is not just about making a screen look responsive. It is about making sure user actions still mean the same thing after time passes and the page reloads. A button moving is not the same thing as a feature working. That was the reminder from this fix.

Top comments (0)