Skip to main content
refactor·June 30, 2025·5 min read

The June sprint, or what coming back looks like

Coming back from a long break, the right first move wasn't a new feature. It was the cleanup that had been bothering me before I left.

J
Jean P.Founder

The first stretch back wasn't a feature.

When I opened the codebase after four months away, the thing that pulled me forward wasn't the next thing to build. It was the chore I'd been ignoring before I left. Three near-identical progress hooks, one for each entity type. Three slightly different toast patterns. A handful of components that re-rendered more than they needed to. Small things, individually. Not small things in aggregate.

Coming back gave me the patience to do the work that hadn't been urgent enough to do before.

The unification was the real work

By the time the entity pipeline shipped, character, monster, and NPC generation each had their own progress tracking hook. Each one polled the same kind of status field. Each one had its own slightly different way of computing whether a generation was done. Each one had its own loading states, its own error handling, its own retry logic.

When I built them, that duplication was fine. The pipeline was new, the entity types were new, and copying a hook to a new entity took five minutes versus the half day it would've cost to abstract it correctly. Premature abstraction is the more expensive mistake. I made the right call at the time.

By June it had stopped being the right call. Three hooks meant three places to fix every bug. Three places to tweak every UI behavior. Three places where I had to remember which one was the most recent source of truth, because I'd evolved them in parallel without keeping them in sync. The kind of low-grade friction that doesn't slow you down on any given day but slows you down every day.

The cleanup was a unified status hook. One file, one source of truth, one set of behaviors. Each entity type plugs into it through a thin adapter that knows how to read its specific status field. The hook itself doesn't know what kind of thing is being generated, and doesn't need to. Same shape as the trigger task layer from the entity pipeline post: the verb composes, the data stays separate.

The same pass cleaned up the toast system. Three near-identical toast patterns became one. A new entity type now plugs into both the progress system and the toast system through the same adapter shape. The fourth entity type, when it shows up, won't grow either of them.

Immediate UX

The thing I wanted out of this work wasn't just deduplication. It was a UX that felt immediate.

Generation flows are async. The user clicks a button, the model takes ten seconds to produce a result, and the time between the click and the result is where the experience either feels alive or feels broken. The unified progress system was a chance to tighten that loop. Status updates render the moment they arrive. Toasts fire on state transitions, not on polling intervals. Progress bars move when something actually moves, and not at all when nothing is moving.

A lot of this is small detail work. The dialogs already worked before the cleanup. Generations already completed. The bar already filled. What changed is that the entire feedback loop now goes through one path that I can reason about, which means I can tighten it without breaking three other places.

The first time I clicked a generate button after the cleanup landed, the responsiveness was noticeable. Not faster, exactly. Just less ambiguous. The UI knew what was happening, told me, and then got out of the way.

The performance pass

The other thread in this sprint was performance. Memoization on a few components that were re-rendering more than they should have been, particularly in the layers panel and the entity sheet headers. CreditBadge fallback handling. Equipment and legendary actions null safety in the monster sheet.

Not deep work. Just the obvious wins you collect when you're already in the file.

The honest framing is that I wanted to work on performance, in the same way that someone returning from a break wants to organize their desk before they start anything new. There wasn't a profiling crisis. There wasn't a user complaint. There was just an opportunity to make the existing surface a little smoother before adding new surface to it.

The upgrade

Somewhere in this stretch I also did the Next.js 15 and React 19 upgrade. It was a chore. It worked. There's nothing more to say about it.

What the stretch shipped

A unified progress tracking system across all entity types. A unified toast system across all entity types. A handful of components that re-render less than they used to. A codebase that was on current major versions of its core dependencies.

What the sprint really shipped, though, was the answer to a question I hadn't asked out loud: what does coming back look like after a long pause? It looks like cleanup. The features were already there. The architecture was already there. What was missing was the small disciplined attention that turns a working product into one that feels good to use.

That part doesn't compound while you're away. You have to put in for it when you come back.

SHARE
~ 5 min read · 866 words
Discord

Discuss this post

Join the D3 Designs Discord to share thoughts and follow along.

Join Discord