Skip to content

Latest commit

 

History

History
134 lines (107 loc) · 8.47 KB

File metadata and controls

134 lines (107 loc) · 8.47 KB

@asos/web-toggle-point-features

This package provides utility modules for managing features state in applications.

It provides global or contextual status of the toggles that should govern relevant modules, passing methods to an appropriate decision-making toggle point (e.g. those created by withTogglePoint or withToggledHook from the react-pointcuts package).

A store should be chosen based on the requirement for global or partitioned state, and the reactivity needed, for the type of toggle.

Usage

See: JSDoc output

Exports

Warning

This package uses package.json exports to specify individual stores (listed below), to ensure that browser / node specific stores can be individually imported and prevent build failures where, prior to tree-shaking, incompatible APIs / globals are referenced. Due to a long-standing bug in eslint-plugin-import, users of eslint with this plugin may need to ignore an import/no-unresolved error, or move to a modern alternative for this plugin (e.g. eslint-plugin-import-x), or use a typescript parser (which understands exports)

The package contains the following exports:

storeFactories/globalFeaturesStoreFactory

A "global" features store factory: a thin wrapper around a singleton, this is an extension point for future plugins etc. Each invocation will create a new store, even with a toggleType matching a prior invocation.

It accepts the following parameters:

  • toggleType
    • the type of the toggle, used only for debugging.

It exports a store with:

  • a setValue function, that sets a current value.
  • a getFeatures function
    • designed to be passed as the getActiveFeatures input of the withTogglePointFactory or withToggledHookFactory from the react-pointcuts package.

For protection against variation (or other) code modifying the toggle state unduly, the value passed could be deep frozen, e.g.

import { deepFreeze } from "deep-freeze-es6";
const initialValue = {};
featuresStore.setValue({ value: deepFreeze(initialValue) });

For reactive values, without the need for a React or other contextual wrapper, consider wrapping an object with valtio:

import { proxy } from "valtio/vanilla";
const initialValue = {};
featuresStore.setValue({ value: proxy(initialValue) });

...which can then be subscribed to in an appropriate toggle point, to re-evaluate toggled functions:

import { subscribe } from "valtio/vanilla";
subscribe(featuresStore.getFeatures(), () => { /* re-evaluate */ });

If using React (e.g. react-pointcuts package), can just use the native support in Valtio:

import { proxy, useSnapshot } from "valtio";
const initialValue = {};
featuresStore.setValue({ value: proxy(initialValue) });
export const setValue = (input) =>  // consumed in updating code-paths
  featuresStore.setValue({
    value: Object.assign(featuresStore.getFeatures(), input)
  });
export const getActiveFeatures = () => useSnapshot(featuresStore.getFeatures()); // passed to `withTogglePointFactory`

...which will then re-render consuming components based on the parts of the toggle state they are reliant on.

storeFactories/nodeRequestScopedFeaturesStoreFactory

A "request scoped" features store factory, for use in Node.

It accepts the following parameters:

  • toggleType
    • the type of the toggle, this is keyed against a singleton referenced by a runtime-wide global symbol, to ensure it works across multi-compilation runtimes (e.g. NextJs) / throughout a realm. N.B. Each invocation with the same toggleType will return the initial store.

It exports a store with:

  • a setValue function that sets a current value, taking a scopeCallBack (along with a value), under which the value is scoped.
    • This is using AsyncLocalStorage.run under the hood, which can be plugged into Express middleware thus:
      import express from "express";
      
      const app = express();
      const featuresStore = requestScopedFeaturesStoreFactory({ toggleType: "some type of toggle" });
      
      app.use((request, response, next) => {
        const value = ?? // some value holding toggle state, either based on `request`, or scoped from outside this middleware, etc.
        featuresStore.setValue({ value, scopeCallBack: next });
      });
      app.use("/", () => { /* routes that require toggled code */ });
  • a getFeatures function
    • designed to be passed as the getActiveFeatures input of the withTogglePointFactory or withToggledHookFactory from the react-pointcuts package.

Warning

This will throw an error if called outside of a request scope, so care should be taken to set up the toggle point config to only toggle modules called within the call stack of the middleware. Wrap the setValue call in a try / catch, if prior access is expected. If this happens unexpectedly, follow the advice here.

storeFactories/reactContextFeaturesStoreFactory

It accepts the following parameters:

  • toggleType
    • the type of the toggle, forming the displayName of the react context provider

It exports a store with:

  • a providerFactory factory function, creating a react context provider.
    • appropriate parts of the react tree, that need to have toggled react components, should be wrapped by this provider. It should be passed a value representing active features state.
  • a getFeatures function
    • this uses useContext internally, so should be used honouring the rules of hooks. It will make consumers reactive to any change of the toggle state.
    • can be passed to getActiveFeatures of withTogglePointFactory / withToggledHookFactory from the react-pointcuts package.

storeFactories/ssrBackedReactContextFeaturesStoreFactory

It exports a store with the same signature as that exported by reactContextFeaturesStoreFactory. It utilises withJsonIsomorphism from the ssr package internally, to create "isomorphic" or "universal" contexts, for use in framework-less React applications. The value set on the server will be realised as the initial value within the browser.

It accepts the following parameters:

  • namespace
    • this becomes a prefix for the id of the application/json script written to the page, useful for pages running multiple react applications.
  • toggleType
    • the type of the toggle, the latter part of the id of the aforementioned script, and becoming the prop holding the features state, and forming the displayName of the underlying react context provider
  • logWarning
    • a method to log warnings, should the serialized json somehow become malformed when hydrating the client application
      • this was designed to allow modifications of markup in systems upstream of the origin, but downstream of the browser, with a view to ensure adequate telemetry is in place.

Warning

Use with React 17

The react-specific stores should work with React 17 and above, but due to a bug that they are not back-filling, the use of "type": "module" in the package means webpack will be unable to resolve the extensionless import. To fix, either upgrade to React 18+ or add the following resolve configuration to the webpack config:

resolve: {
  alias: {
    "react/jsx-runtime": "react/jsx-runtime.js",
    "react-dom/server": "react-dom/server.js",
  }
}