@clagradi/effect-runtime
v0.1.2
Published
Better async effect primitive for React 18+
Downloads
304
Maintainers
Readme
effect-runtime
@clagradi/effect-runtime is a small React 18+ primitive for safer async effects.
Install
npm i @clagradi/effect-runtimepnpm add @clagradi/effect-runtimeyarn add @clagradi/effect-runtimeWhy better than useEffect
- Per-run
AbortController(auto abort on rerun/unmount) commit()anti-race guard for stale async completions- Late async cleanup is still executed after dispose
onCleanup()supports multiple cleanups with LIFO orderuseEventgives stable callbacks without stale closures- StrictMode-safe effect lifecycle behavior
Before / after
Before:
useEffect(() => {
const controller = new AbortController();
let cancelled = false;
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((response) => response.json())
.then((user) => {
if (!cancelled) {
setName(user.name);
}
});
return () => {
cancelled = true;
controller.abort();
};
}, [userId]);After:
useEffectTask(
async ({ signal, commit }) => {
const response = await fetch(`/api/users/${userId}`, { signal });
const user = await response.json();
commit(() => {
setName(user.name);
});
},
[userId]
);Examples
FetchDemo
Uses signal + commit so overlapping requests do not commit stale state.
useEffectTask(
async ({ signal, commit }) => {
const response = await fetch(`/api/todos/${todoId}`, { signal });
const todo = await response.json();
commit(() => {
setTodo(todo);
});
},
[todoId]
);IntervalDemo
Uses useEvent so an interval sees the latest state without growing dependency arrays.
const onTick = useEvent(() => {
setCount((value) => value + 1);
console.log(label);
});
useEffectTask(({ onCleanup }) => {
const handle = setInterval(() => onTick(), 1000);
onCleanup(() => clearInterval(handle));
}, []);SubscriptionDemo
Uses onCleanup to pair subscribe / unsubscribe logic per run.
useEffectTask(({ onCleanup }) => {
const unsubscribe = subscribeToRoom(roomId, (message) => {
setMessages((prev) => [message, ...prev].slice(0, 5));
});
onCleanup(() => unsubscribe());
}, [roomId]);Run the example app locally
cd examples/vite-react
npm install
npm run devThen open the local URL printed by Vite, usually http://localhost:5173.
Try it online:
- StackBlitz:
TODO - CodeSandbox:
TODO
The example app depends on the published package, and the release smoke test temporarily installs the packed tarball on top of it. Examples are not published to npm because the root package ships only dist/.
API
export function useEvent<T extends (...args: any[]) => any>(fn: T): T;
type EffectTaskScope = {
signal: AbortSignal;
runId: number;
isActive(): boolean;
commit(fn: () => void): void;
onCleanup(fn: () => void): void;
};
type EffectTask =
(scope: EffectTaskScope) =>
void | (() => void) | Promise<void | (() => void)>;
export function useEffectTask(
task: EffectTask,
deps: any[],
options?: { layout?: boolean; onError?: (err: unknown) => void; debugName?: string }
): void;Caveats
- Not a data-fetching cache. This is not React Query or SWR.
- No caching or SSR orchestration.
- It helps effect correctness; it does not replace application data architecture.
How to verify the package as a consumer
Run the real consumer smoke test used by CI:
npm run smoke:consumerWhat it does:
- builds
dist/from source - builds the library tarball with
npm pack - installs that tarball into
examples/vite-react - runs the example app
typecheck - runs the example app
build
You can also do it manually:
npm run build
npm pack --pack-destination .tmp
cd examples/vite-react
npm install --package-lock=false
npm install --no-save --package-lock=false ../../.tmp/clagradi-effect-runtime-<version>.tgz
npm run typecheck
npm run buildHow release works
- Validate the repo:
npm run typecheck
npm run test
npm run build
npm run smoke:consumer- Bump the version and create the release tag:
npm version patch- Push
mainand the tag:
git push origin main
git push origin --tags- GitHub Actions publishes on tags matching
v*using:
npm publish --provenance --access public- Local manual publish remains available, but CI publish is the preferred path. If local npm tries to generate provenance outside a supported provider, use:
npm publish --access public --provenance=false