GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - mcrovero/effect-react-cache: Memoize Effect computations with React’s cache: argument-aware, concurrent-safe, typed.

BranchesTags
Open more actions menu

Latest commit

 

History

20 Commits
20 Commits

Folders and files

NameName
Last commit message
Last commit date
.changeset
.changeset
 
 
.github
.github
 
 
.vscode
.vscode
 
 
example
example
 
 
fixtures/next-app
fixtures/next-app
 
 
integration
integration
 
 
patches
patches
 
 
scratchpad
scratchpad
 
 
src
src
 
 
test
test
 
 
.cursorrule
.cursorrule
 
 
.gitignore
.gitignore
 
 
.prettierignore
.prettierignore
 
 
.prettierrc.json
.prettierrc.json
 
 
CHANGELOG.md
CHANGELOG.md
 
 
LICENSE
LICENSE
 
 
README.md
README.md
 
 
eslint.config.mjs
eslint.config.mjs
 
 
package.json
package.json
 
 
pnpm-lock.yaml
pnpm-lock.yaml
 
 
setupTests.ts
setupTests.ts
 
 
tsconfig.base.json
tsconfig.base.json
 
 
tsconfig.build.json
tsconfig.build.json
 
 
tsconfig.examples.json
tsconfig.examples.json
 
 
tsconfig.integration.json
tsconfig.integration.json
 
 
tsconfig.json
tsconfig.json
 
 
tsconfig.src.json
tsconfig.src.json
 
 
tsconfig.test.json
tsconfig.test.json
 
 
vitest.config.ts
vitest.config.ts
 
 
vitest.next.config.ts
vitest.next.config.ts
 
 

Repository files navigation

@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 server 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 during a React Server Component render. 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
// React memoization only happens when this Effect is executed
// from an active React server render.
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)

// When executed inside the same React server render:
// 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
  })
)

// Inside the same React server render, the 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: <F extends (...args: Array<any>) => Effect.Effect<any, any, any>>(
  effect: F
) => (...args: Parameters<F>) => ReturnType<F>
  • 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 completed Exit is reused for the lifetime of the current React request/render cache.

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 server render, you may want to mock react’s cache to ensure deterministic, in-memory memoization. React’s default non-server build treats cache as a passthrough, so plain Effect.runPromise(...) calls will not memoize on their own. A simple primitive-oriented mock looks like this:

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 a more faithful identity-based mock and 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 primitives or stable object identities 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 React Server Components. Outside a React server render, react’s default cache implementation is effectively a passthrough.

Works with Next.js

You can use this library together with @mcrovero/effect-nextjs to deduplicate Effect-based functions within the same Next.js server render across pages, layouts, and server components.

About

Memoize Effect computations with React’s cache: argument-aware, concurrent-safe, typed.

Resources

Readme
MIT license
Activity

Stars

15 stars

Watchers

0 watching

Forks

0 forks
Report repository

Releases

Packages

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.