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

@mcrovero/effect-react-cache

v0.2.3

Published

A React cache wrapper in Effect

Downloads

395

Readme

@mcrovero/effect-react-cache

npm version license: MIT

This library is in early alpha and not yet ready for production use.

Typed helpers to compose React’s cache with Effect in a type-safe, ergonomic way.

Install

pnpm add @mcrovero/effect-react-cache effect react

Why

React exposes a low-level cache primitive to memoize async work by argument tuple. This library wraps an Effect-returning function with React’s cache so you can:

  • Deduplicate concurrent calls: share the same pending promise across callers
  • Memoize by arguments: same args → same result without re-running the effect
  • Keep Effect ergonomics: preserve R requirements and typed errors

Quick start

import { Effect } from "effect"
import { reactCache } from "@mcrovero/effect-react-cache/ReactCache"

// 1) Wrap an Effect-returning function
const fetchUser = (id: string) =>
  Effect.gen(function* () {
    yield* Effect.sleep(200)
    return { id, name: "Alice" as const }
  })

const cachedFetchUser = reactCache(fetchUser)

// 2) Use it like any other Effect
await Effect.runPromise(cachedFetchUser("u-1"))

Usage

Cache a function with arguments

import { Effect } from "effect"
import { reactCache } from "@mcrovero/effect-react-cache/ReactCache"

const getUser = (id: string) =>
  Effect.gen(function* () {
    yield* Effect.sleep(100)
    return { id, name: "Alice" as const }
  })

export const cachedGetUser = reactCache(getUser)

// Same args → computed once, then memoized
await Effect.runPromise(cachedGetUser("42"))
await Effect.runPromise(cachedGetUser("42")) // reuses cached promise

Cache a function without arguments

import { Effect } from "effect"
import { reactCache } from "@mcrovero/effect-react-cache/ReactCache"

export const cachedNoArgs = reactCache(() =>
  Effect.gen(function* () {
    yield* Effect.sleep(100)
    return { ok: true as const }
  })
)

Cache with R requirements (Context)

import { Context, Effect } from "effect"
import { reactCache } from "@mcrovero/effect-react-cache/ReactCache"

class Random extends Context.Tag("MyRandomService")<Random, { readonly next: Effect.Effect<number> }>() {}

export const cachedWithRequirements = reactCache(() =>
  Effect.gen(function* () {
    const random = yield* Random
    const n = yield* random.next
    return n
  })
)

// First call for a given args tuple determines the cached value
await Effect.runPromise(cachedWithRequirements().pipe(Effect.provideService(Random, { next: Effect.succeed(111) })))

// Subsequent calls with the same args reuse the first result,
// even if a different Context is provided!
await Effect.runPromise(cachedWithRequirements().pipe(Effect.provideService(Random, { next: Effect.succeed(222) })))

API

declare const reactCache: <A, E, R, Args extends Array<unknown>>(
  effect: (...args: Args) => Effect.Effect<A, E, NoScope<R>>
) => (...args: Args) => Effect.Effect<A, E, NoScope<R>>
  • Input: an Effect-returning function
  • Output: a function with the same signature, whose evaluation is cached by argument tuple using React’s cache

How it works

  • Internally uses react/cache to memoize by the argument tuple.
  • For each unique args tuple, the first evaluation creates a single promise that is reused by all subsequent calls (including concurrent calls).
  • The Effect context (R) is captured at call time, but for a given args tuple the first successful or failed promise is reused for the lifetime of the process.

Important behaviors

  • First call wins: for the same args tuple, the first call’s context and outcome (success or failure) are cached. Later calls with a different context still reuse that result.
  • Errors are cached: if the first call fails, the rejection is reused for subsequent calls with the same args tuple.
  • Concurrency is deduplicated: concurrent calls with the same args share the same pending promise.

Do's and Don'ts

  • Do: cache pure/idempotent computations that return plain data.
  • Do: include discriminators (locale, tenant, user) in the argument tuple when results depend on them.
  • Don't: pass effects that require Scope or create live resources (DB/client handles, file handles, sockets). Acquire resources outside and provide them, or use a Layer.
  • Don't: rely on per-call timeouts/cancellation or different Context for the same args. The first call determines the cached outcome and context.

Limitations

  • No scoped resources: Effects requiring Scope are rejected at the type level. React's cache evaluates once and reuses the result, so any scoped resource would be finalized immediately after creation, breaking later callers.
  • First call wins: For a given args tuple, the first call's context and outcome (success or failure) are cached and reused.
  • Context sensitivity: If results depend on request context (logger level, locale, tracer span, etc.), include those discriminators in the arguments or avoid caching.
  • Streams/Channels: Don't cache effects that return live Stream/Channel handles tied to resources.

Testing

When running tests outside a React runtime, you may want to mock react’s cache to ensure deterministic, in-memory memoization:

import { vi } from "vitest"

vi.mock("react", () => {
  return {
    cache: <F extends (...args: Array<any>) => any>(fn: F) => {
      const memo = new Map<string, ReturnType<F>>()
      return ((...args: Array<any>) => {
        const key = JSON.stringify(args)
        if (!memo.has(key)) {
          memo.set(key, fn(...args))
        }
        return memo.get(key) as ReturnType<F>
      }) as F
    }
  }
})

See test/ReactCache.test.ts for examples covering caching, argument sensitivity, context provisioning, and concurrency.

Caveats and tips

  • The cache is keyed by the argument tuple using React’s semantics. Prefer using primitives or stable/serializable values as arguments.
  • Since the first outcome is cached, design your effects such that this is acceptable for your use case. For context-sensitive computations, include discriminators in the argument list.
  • This library is designed for server-side usage (e.g., React Server Components / server actions) where React’s cache is meaningful.

Works with Next.js

You can use this library together with @mcrovero/effect-nextjs to cache Effect-based functions between Next.js pages, layouts, and server components.