Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

unctx

unjs2.4mMIT2.4.1TypeScript support: included

Composition-api in Vanilla js

readme

🍦 unctx

Composition-API in Vanilla js

npm version npm downloads package phobia bundle phobia codecov

What is unctx?

Vue.js introduced an amazing pattern called Composition API that allows organizing complex logic by splitting it into reusable functions and grouping in logical order. unctx allows easily implementing composition API pattern in your javascript libraries without hassle.

Usage

In your awesome library:

yarn add unctx
# or
npm install unctx
import { createContext } from "unctx";

const ctx = createContext();

export const useAwesome = ctx.use;

// ...
ctx.call({ test: 1 }, () => {
  // This is similar to the vue setup function
  // Any function called here can use `useAwesome` to get { test: 1 }
});

User code:

import { useAwesome } from "awesome-lib";

// ...
function setup() {
  const ctx = useAwesome();
}

Note: When no context is presented ctx.use will throw an error. Use ctx.tryUse for tolerant usages (return nullable context).

Using Namespaces

To avoid issues with multiple version of the library, unctx provides a safe global namespace to access context by key (kept in globalThis). Important: Please use a verbose name for the key to avoid conflict with other js libraries. Using the npm package name is recommended. Using symbols has no effect since it still causes multiple context issues.

import { useContext, getContext } from "unctx";

const useAwesome = useContext("awesome-lib");

// or
// const awesomeContext = getContext('awesome-lib')

You can also create your internal namespace with createNamespace utility for more advanced use cases.

Async Context

Using context is only possible in non-async usages and only before the first await statement. This is to make sure context is not shared between concurrent calls.

async function setup() {
  console.log(useAwesome()); // Returns context
  setTimeout(() => {
    console.log(useAwesome());
  }, 1); // Returns null
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log(useAwesome()); // Returns null
}

A simple workaround is caching context into a local variable:

async function setup() {
  const ctx = useAwesome(); // We can directly access cached version of ctx
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log(ctx);
}

This is not always an elegant and easy way by making a variable and passing it around. After all, this is the purpose of unctx to make sure context is magically available everywhere in composables!

Native Async Context

Unctx supports Node.js AsyncLocalStorage as a native way to preserve and track async contexts. To enable this mode, you need to set asyncContext: true option and also provides an implementation for AsyncLocalStorage (or provide globalThis.AsyncLocalStorage polyfill).

See tc39 proposal for async context and cloudflare docs for relevant platform specific docs.

import { createContext } from "unctx";
import { AsyncLocalStorage } from "node:async_hooks";

const ctx = createContext({
  asyncContext: true,
  AsyncLocalStorage,
});

ctx.call("123", () => {
  setTimeout(() => {
    // Prints 123
    console.log(ctx.use());
  }, 100);
});

Async Transform

Since native async context is not supported in all platforms yet, unctx provides a build-time solution that transforms async syntax to automatically restore context after each async/await statement. This requires using a bundler such as Rollup, Vite, or Webpack.

Import and register transform plugin:

import { unctxPlugin } from "unctx/plugin";

// Rollup
// TODO: Add to rollup configuration
unctxPlugin.rollup();

// Vite
// TODO: Add to vite configuration
unctxPlugin.vite();

// Webpack
// TODO: Add to webpack configuration
unctxPlugin.webpack();

Use ctx.callAsync instead of ctx.call:

await ctx.callAsync("test", setup);

NOTE: callAsync is not transformed by default. You need to add it to the plugin's asyncFunctions: [] option to transform it.

Any async function that requires context, should be wrapped with withAsyncContext:

import { withAsyncContext } from "unctx";

const setup = withAsyncContext(async () => {
  console.log(useAwesome()); // Returns context
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log(useAwesome()); // Still returns context with dark magic!
});

Singleton Pattern

If you are sure it is safe to use a shared instance (not depending to request), you can also use ctx.set and ctx.unset for a singleton pattern.

Note: You cannot combine set with call. Always use unset before replacing the instance otherwise you will get Context conflict error.

import { createContext } from "unctx";

const ctx = createContext();
ctx.set(new Awesome());

// Replacing instance without unset
// ctx.set(new Awesome(), true)

export const useAwesome = ctx.use;

Typed Context

A generic type exists on all utilities to be set for instance/context type for typescript support.

// Return type of useAwesome is Awesome | null
const { use: useAwesome } = createContext<Awesome>();

Under the hood

The composition of functions is possible using temporary context injection. When calling ctx.call(instance, cb), instance argument will be stored in a temporary variable then cb is called. Any function inside cb, can then implicitly access the instance by using ctx.use (or useAwesome)

Pitfalls

context can be only used before first await:

Please check Async Context section.

Context conflict error:

In your library, you should only keep one call() running at a time (unless calling with the same reference for the first argument)

For instance, this makes an error:

ctx.call({ test: 1 }, () => {
  ctx.call({ test: 2 }, () => {
    // Throws error!
  });
});

License

MIT. Made with 💖

changelog

Changelog

All notable changes to this project will be documented in this file. See standard-version for commit guidelines.

v2.4.1

compare changes

🩹 Fixes

  • Prefer asyncContext instance over currentInstance (#101)

🏡 Chore

❤️ Contributors

v2.4.0

compare changes

🚀 Enhancements

🩹 Fixes

  • transform: Don't terminate single if statements (#97)

💅 Refactors

📖 Documentation

  • Add note about adding callAsync to asyncFunctions (#94)

🏡 Chore

✅ Tests

❤️ Contributors

v2.3.1

compare changes

🩹 Fixes

  • transform: Insert code rather than overwriting chunk (#77)

🏡 Chore

❤️ Contributors

v2.3.0

compare changes

🚀 Enhancements

  • Support native async context (#73)

📖 Documentation

🏡 Chore

❤️ Contributors

  • Pooya Parsa (@pi0)

v2.2.0

compare changes

🚀 Enhancements

  • Support transformation of object properties (#69)

🏡 Chore

❤️ Contributors

v2.1.2

compare changes

🏡 Chore

  • Update dependencies and repo (57a6e2b)

❤️ Contributors

2.1.1 (2022-11-28)

Bug Fixes

2.1.0 (2022-11-14)

Features

  • createNamespace: allow creating typed namespace (#31) (fcf580b)

2.0.2 (2022-08-29)

Bug Fixes

2.0.1 (2022-08-04)

Bug Fixes

  • pkg: /transform subpath is also esm-only (b7f63fc), closes #25

2.0.0 (2022-08-03)

⚠ BREAKING CHANGES

  • deps: update estree-walker to v3 (#23)
  • strict ctx.use and new ctx.tryUse (#7)

Features

  • strict ctx.use and new ctx.tryUse (#7) (b54dbd3)
  • deps: update estree-walker to v3 (#23) (f88439e)

1.1.4 (2022-03-31)

1.1.3 (2022-03-25)

Bug Fixes

  • plugin: generate map correctly (d9350b7)
  • plugin: webpack compatible (b4572a4)
  • transform: allow overriding createTransformer (7ef74ff)
  • transform: disable transforming callAsync by default (#5) (dc0fcfd)

1.1.2 (2022-03-24)

Bug Fixes

1.1.1 (2022-03-24)

Bug Fixes

1.1.0 (2022-03-22)

Features

  • transfromer for async context support (#4) (bc4d17a)

1.0.2 (2021-08-24)

Bug Fixes

1.0.1 (2021-06-24)

1.0.0 (2021-06-24)

0.0.4 (2021-06-24)

Features

  • singleton pattern support (f506172)

Bug Fixes

  • unset context if callback throws immediate error (f045049)

0.0.3 (2021-03-30)

Bug Fixes

  • add missing return to createNamespace (04886ef)

0.0.2 (2021-03-30)

Features

0.0.1 (2021-03-30)

Features