npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-phase

v1.0.2

Published

A phase-first lifecycle abstraction for React

Readme

react-phase

A phase-first lifecycle abstraction for React.

react-phase helps you manage component lifecycle logic without directly depending on useEffect. Instead of scattering side effects across multiple effects, react-phase provides structured lifecycle phases with readable and controllable update execution — fully typed with TypeScript.


Why?

Managing side effects in React often becomes difficult because:

  • useEffect mixes multiple concerns
  • dependency arrays become hard to reason about
  • lifecycle intent is unclear
  • update conditions are limited to OR behavior
  • async effects become messy
  • complex effects become unreadable

react-phase provides:

  • explicit lifecycle phases
  • mount / unmount / update separation
  • AND / OR dependency execution
  • custom dependency matchers via .when()
  • debounce & throttle support
  • async phase orchestration
  • request cancellation via AbortSignal
  • retry handling
  • full TypeScript support

Installation

npm install react-phase

pnpm add react-phase

yarn add react-phase

Quick Example

import { usePhase } from "react-phase";

function App({ user, token, search }: Props) {
  const { onMount, onUnmount, onUpdate } = usePhase();

  onMount(() => {
    console.log("mounted");
  });

  onUnmount(() => {
    console.log("cleanup");
  });

  onUpdate([user, token] as const, () => {
    console.log("both changed");
  }).and();

  onUpdate([search] as const, () => {
    console.log("searching...");
  }).debounce(500);

  return <div>Hello</div>;
}

Core Concepts

react-phase organizes component side effects into explicit lifecycle phases.

| Phase | Purpose | |-------------|-------------------------------| | onMount | Runs once after mount | | onUnmount | Runs once before unmount | | onUpdate | Runs when dependencies change |


Lifecycle Phases

onMount

Runs once after component mount. Supports optional cleanup via return value.

onMount(() => {
  const timer = setInterval(tick, 1000);
  return () => clearInterval(timer); // cleanup
});

onUnmount

Runs once before component unmount.

onUnmount(() => {
  console.log("cleanup");
});

onUpdate

Runs when dependencies change. Returns a chainable PhaseController.

onUpdate([count] as const, () => {
  console.log("count changed");
});

Dependency Execution Modes

Default — OR Mode

By default, updates run when any dependency changes.

onUpdate([user, token] as const, () => {
  console.log("either changed");
});

// equivalent to:
onUpdate([user, token] as const, () => {
  console.log("either changed");
}).or();

AND Mode

Run only when all dependencies change.

onUpdate([user, token] as const, () => {
  console.log("both changed");
}).and();

Custom Matchers — .when()

Create fully custom dependency execution logic using .when().

.when() takes priority over .and() and .or().

onUpdate([price] as const, () => {
  console.log("threshold crossed");
}).when((prev, current) => {
  return prev[0] < 1000 && current[0] >= 1000;
});

Authentication example:

onUpdate([user, token] as const, () => {
  console.log("authenticated");
}).when((prev, current) => {
  const [prevUser, prevToken] = prev;
  const [currentUser, currentToken] = current;
  return !prevUser && !prevToken && !!currentUser && !!currentToken;
});

The matcher receives strongly-typed prev and current arrays matching the shape of the deps tuple.


Debounce Updates

Delay execution until dependency changes stop for a specified duration.

Useful for: search inputs, API calls, expensive computations, resize handlers.

onUpdate([search] as const, () => {
  fetchResults(search);
}).debounce(500);

The update runs only after 500ms of inactivity.


Throttle Updates

Limit how frequently updates can execute.

Useful for: scroll events, mouse movement, rapid state updates, performance-sensitive operations.

onUpdate([scrollY] as const, () => {
  console.log(scrollY);
}).throttle(200);

The update runs at most once every 200ms.


Async Phase Orchestration

react-phase supports async lifecycle execution with built-in orchestration helpers.

Async Updates

onUpdate([userId] as const, async () => {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  setUser(data);
});

Automatic Stale Execution Protection

When dependencies change rapidly, older async executions are automatically ignored — only the latest execution is applied.

Abort Support

Async phases receive an AbortSignal. When the component unmounts, dependencies change, or execution becomes stale, the previous request is aborted automatically.

onUpdate([userId] as const, async ({ signal }) => {
  const response = await fetch(`/api/users/${userId}`, { signal });
  const data = await response.json();
  setUser(data);
});

Retry Support

Retries failed executions up to N times with exponential backoff.

onUpdate([userId] as const, async () => {
  return fetchUser(userId);
}).retry(3);

Error Handling

onUpdate([userId] as const, async () => {
  return fetchUser(userId);
}).catch((error) => {
  console.error(error);
});

Async State

The controller exposes reactive loading/error/success state.

const request = onUpdate([userId] as const, async () => {
  return fetchUser(userId);
});

console.log(request.loading);
console.log(request.error);
console.log(request.success);

Combined Example

import { usePhase } from "react-phase";

function Dashboard({ user, token, search, scrollY, price }: Props) {
  const { onMount, onUnmount, onUpdate } = usePhase();

  onMount(() => {
    console.log("Dashboard mounted");
  });

  onUnmount(() => {
    console.log("Dashboard cleanup");
  });

  onUpdate([user, token] as const, () => {
    console.log("Both changed");
  }).and();

  onUpdate([search] as const, async ({ signal }) => {
    const response = await fetch(`/api/search?q=${search}`, { signal });
    return response.json();
  })
    .debounce(400)
    .retry(2)
    .catch(console.error);

  onUpdate([scrollY] as const, () => {
    console.log("Tracking scroll...");
  }).throttle(100);

  onUpdate([price] as const, () => {
    console.log("Threshold crossed");
  }).when((prev, current) => {
    return prev[0] < 1000 && current[0] >= 1000;
  });

  return <div>Dashboard</div>;
}

Individual Hooks

useMount

import { useMount } from "react-phase";

function App() {
  const onMount = useMount();

  onMount(() => {
    console.log("mounted");
  });

  return <div>Hello</div>;
}

useUnmount

import { useUnmount } from "react-phase";

function App() {
  const onUnmount = useUnmount();

  onUnmount(() => {
    console.log("cleanup");
  });

  return <div>Hello</div>;
}

useUpdate

import { useUpdate } from "react-phase";

function App() {
  const onUpdate = useUpdate();

  onUpdate([count] as const, () => {
    console.log("updated");
  });

  return <div>Hello</div>;
}

Fluent API

onUpdate() returns a chainable phase controller.

| Method | Description | |-------------------|------------------------------------| | .and() | Run when ALL dependencies change | | .or() | Run when ANY dependency changes | | .when(fn) | Custom dependency matcher | | .debounce(ms) | Delay execution | | .throttle(ms) | Limit execution frequency | | .retry(count) | Retry failed async phases | | .catch(handler) | Handle async errors |


API

usePhase

const { onMount, onUnmount, onUpdate } = usePhase();

onMount(callback)

Runs once after mount. Return a function for cleanup.

onUnmount(callback)

Runs once before unmount.

onUpdate(deps, callback)

Returns a chainable PhaseController<T>.

PhaseController<T>

interface PhaseController<T extends Deps> {
  loading: boolean;
  error: unknown | null;
  success: boolean;

  and(): this;
  or(): this;
  when(fn: MatcherFn<T>): this;
  debounce(ms: number): this;
  throttle(ms: number): this;
  retry(count: number): this;
  catch(handler: (error: unknown) => void): this;
}

Design Philosophy

react-phase is designed to:

  • embrace React
  • reduce useEffect complexity
  • improve readability
  • provide smarter dependency handling
  • simplify async effects
  • remain lightweight and predictable

This library does not replace React internally. It provides a cleaner lifecycle abstraction layer on top of React's lifecycle system.


Roadmap

v1

  • [x] mount phase
  • [x] unmount phase
  • [x] update phase
  • [x] AND dependency execution
  • [x] OR dependency execution
  • [x] custom dependency matchers via .when()
  • [x] debounce support
  • [x] throttle support
  • [x] async orchestration
  • [x] retry handling with exponential backoff
  • [x] request cancellation via AbortSignal
  • [x] full TypeScript support

Future Ideas

  • lifecycle devtools
  • transition scheduling
  • update batching
  • execution tracing
  • previous dependency snapshots
  • custom schedulers
  • middleware system
  • plugin ecosystem

License

MIT