Add the before lifecycle hook (#324)#365
Conversation
ff0b4a6 to
db07692
Compare
`Context.before(callback)` registers a callback that fires immediately before a component re-renders, and only on re-renders (not the initial render). Useful for cancelling in-flight async work from the previous render before it goes stale — e.g. aborting a fetch via AbortController. Re-register it each render. Unlike `refresh`, `schedule`, and `after`, it never itself triggers a re-render.
db07692 to
866d564
Compare
Addresses review on #365: the verbose runBeforeCallbacks name and its commentary weren't pulling their weight. The two call-sites (an explicit refresh and a mounted re-render) stay — the initial-mount path deliberately doesn't fire before callbacks, so it can't fold into enqueueComponent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
||
| ```js | ||
| this.refresh(callback?) // Mutate state and re-render | ||
| this.before(callback) // Run before the next re-render (e.g. abort stale async work) |
There was a problem hiding this comment.
@claude One of these methods is not like the other one (zero-arg promise form)
There was a problem hiding this comment.
Right — verified in the impl. schedule/after/cleanup each carry a second overload where the zero-arg call returns Promise<TResult> and resolves with the rendered value at that lifecycle moment (their callbacks take (value: TResult)). before is before(callback: () => unknown): void — single signature, and its callback takes no value, because it fires before any render exists. So there is no TResult to resolve a promise with; a before(): Promise could only resolve with void, which is the asymmetry rather than a fix for it. (And await this.before() would largely duplicate for await...of this as a “wait for the next update” signal.)
So it is a genuine fork, your call:
- Keep it callback-only and make the doc asymmetry intentional (drop the implied
?, maybe a one-word note). - Add
before(): Promise<void>for surface symmetry, accepting the void resolution.
The SKILL.md line is accurate to (1) as written. Say which way and I’ll align the doc (and the overload if you want 2).
There was a problem hiding this comment.
Done in abb5f9f — added the before(): Promise<void> overload (mirrors the schedule idiom: new Promise((resolve) => this.before(() => resolve()))). Updated the SKILL.md signature to before(callback?), the CHANGELOG, and added a test asserting it stays pending until a re-render then resolves with undefined. The lone remaining asymmetry — resolving with void rather than the rendered value — is inherent, since nothing has rendered yet.
Matches schedule/after/cleanup: calling before() with no argument now returns a promise that resolves just before the next re-render. It resolves with no value, since no render has happened yet — the one way it still differs from the others, which resolve with the rendered value. Updates the SKILL.md signature and CHANGELOG accordingly, plus a test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #324.
Adds
Context.before(callback)— a lifecycle hook that fires immediately before a component re-renders, and only on re-renders (not the initial render). It lets a component cancel in-flight async work from the previous render before it becomes stale.Use case
When
userIdchanges,beforefires and aborts the stale fetch before the loop re-iterates.Semantics
schedule.refresh/schedule/after).Implementation
A
beforeMapkeyed by context state (mirroringscheduleMap/cleanupMap), plusrunBeforeCallbacks(ctx)invoked at the two synchronous re-render request points:diffComponent's update branch (prop changes / parent re-renders) andrefresh(). Callbacks are cleared after firing.Tests
test/before.tsxcovers: no-fire on initial render; ordering before re-render (refresh + prop change); once-per-registration; no self-triggered re-render; the AbortController use case; and the async-generatorfor awaitpath. Full suite green (580);tsc --noEmitclean.🤖 Generated with Claude Code