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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@interbolt/selectable

v0.1.0-alpha.0

Published

A proof of concept API to enable (theoretically render-optimized) composable selector hooks based on Context values and hook returns.

Downloads

3

Readme

⚡ @interbolt/selectable - (theoretically) Performant and composable hook selectors

A proof of concept API to enable (theoretically render-optimized) composable selector hooks based on Context values and hook returns.

Disclaimer: 🚧 this lib only demonstrates the API, but requires more work from the core React team before it can deliver on any render-optimization claims.

Requirements to make render-optimization work

  • [ ] release of React version 18.3 or greater which will include the React.use hook
  • [ ] support for calling hooks in React.useMemo

TODOS required to release v0.1.0

  • [ ] add types
  • [ ] minimal test suite

Table of Contents

Usage

Example 1) make a context "selectable".

import { createContext } from "react";

const useApp = () => {
  const timeInSeconds = useTimeInSeconds();
  const isDarkOutside = useIsDarkOutside();

  return {
    theme: {
      colors: {
        dark: "black",
        light: "white",
      },
      mode: isDarkOutside ? "dark" : "light",
    },
    timeInSeconds,
  };
};

const Context = createContext(null);

// This can be called within a render function likeso:
// `const mode = useAppSelector(theme => theme.mode)`
// or further narrowed, as seen in the code below.
const useAppSelector = selectable(Context);

// Narrow the portion of the context we want to select from.
const useThemeSelector = useAppSelector.narrow((ctx) => ctx.theme);

// 🎉 Composable! author of `useDarkModeColorSelector` doesn't need to know the
// structure of the entire `ctx` value.
const useDarkModeColorSelector = useThemeSelector.narrow(
  (theme) => theme.colors.dark
);

// NOTE: once React adds the useMemo hook/context optimizations,
// none of the hooks calls below should trigger rerenders when
// `ctx.timeInSeconds` changes.
const DisplayColorOptions = () => {
  const selectLightColor = useCallback((colors) => colors.dark);

  const darkModeColor = useDarkModeColorSelector();
  const lightModeColor = useColorsSelector(selectLightColor);

  return (
    <div>
      <p>Light mode color: {lightModeColor}</p>
      <p>Dark mode color: {darkModeColor}</p>
    </div>
  );
};

const AppProvider = () => {
  const app = useApp();

  return (
    <Context.Provider value={app}>
      <DisplayColorOptions />
    </Context.Provider>
  );
};

Example 2) make a hook "selectable".

import { createContext } from "react";

// This can be called within a render function likeso:
// `const mode = useAppSelector(theme => theme.mode)`
// or further narrowed, as seen in the code below.
const useAppSelector = selectable(useApp);

// Narrow the portion of the `useApp` return value we want to select from.
const useThemeSelector = useAppSelector.narrow((ctx) => ctx.theme);

// 🎉 Composable! author of `useDarkModeColorSelector` doesn't need to know the
// structure of the entire `ctx` value.
const useDarkModeColorSelector = useThemeSelector.narrow(
  (theme) => theme.colors.dark
);

// NOTE: once React adds the useMemo hook/context optimizations,
// none of the hooks calls below should trigger rerenders when
// `ctx.timeInSeconds` changes.
const DisplayColorOptions = () => {
  const selectLightColor = useCallback((colors) => colors.dark);

  const darkModeColor = useDarkModeColorSelector();
  const lightModeColor = useColorsSelector(selectLightColor);

  return (
    <div>
      <p>Light mode color: {lightModeColor}</p>
      <p>Dark mode color: {darkModeColor}</p>
    </div>
  );
};

API

@interbolt/selectable's default export is a function called selectable.

Any hook produced by calling selectable will have a .narrow method attached to it (remember, fns are just objs).

The narrow method will return a useSelector(selector)-style hook based on the selector function provided to narrow. Compositions of new "narrowed" selector hooks are not constrained to using only the original hook or context provided to selectable. The narrow method can take any number of other context or hooks as params and pass their values to the "narrowing" selector.

That's all a little confusing so let's look at the ways we can use selectable and .narrow:

import { UserContext, ThemeContext } from "./myAppsContexts";
import {
  useDashboardFeatures,
  useFeatureFlags,
  useAnalytics,
} from "./myAppsHooks";

// EX: 1️⃣ - create a selectable user context and then
// use the `narrow` method to get their preferred colors by
// by composing `useUserSelector` with `ThemeContext`.

// Step 1 - create the selector hook from the `UserContext`
const useUserSelector = selectable(UserContext);

// Step 2 - create a hook that we can use to select from
// the user's preferred colors. See how we were able to
// use the `ThemeContext` by passing it in as the first param
// to `.narrow`.
const usePreferredColorsSelector = useUserSelector.narrow(
  ThemeContext,
  (theme, user) => theme.colors[user.colorPreference]
);

// EX: 2️⃣ - create a selectable user context and then
// use the `useFeatureFlags` and `useDashboardFeatures` hooks
// to create a hook that will tell us which features our UI should
// suggest the user try on their dashboard page.

// Step 1 - create the selector hook from the `UserContext`
const useUserSelector = selectable(UserContext);

// Step 2 - include the return values of `useFeatureFlags` and
// `useDashboardFeatures` in the selector param so that we can
// determine which active dashboard features we should suggest
// the user try.
const useDashboardFeaturesToSuggest = useUserSelector.narrow(
  useFeatureFlags,
  useDashboardFeatures,
  (featureFlags, dashboardFeatures, user) => {
    const { activeFeatures } = featureFlags;
    const { acknowledgedFeatures } = user;
    return _.difference(acknowledgedFeatures, activeFeatures);
  }
);

// Now go crazy - what if we want to change the color of something
// in our UI based on how many pending suggestions a user has
// on the dashboard page.
const useFeatureSuggestionsColor = useDashboardFeaturesToSuggest.narrow(
  ThemeContext,
  (theme, suggestions) => {
    if (suggestions.length === 0) {
      return theme.colors.pendingSuggestions.none;
    }

    // maybe a warning orange?
    if (suggestions.length > 0 && suggestions.length < 3) {
      return theme.colors.pendingSuggestions.some;
    }

    // maybe show a danger red?
    return theme.colors.pendingSuggestions.lots;
  }
);

// The author of `useFeatureSuggestionsColor` doesn't need to care
// at all about how `useDashboardFeaturesToSuggest`
// was implemented. Composability!!! 🎉🎉🎉

Demo

I included a demo in the demo/ folder. To get it running do:

cd demo
npm ci
npm run start

Then navigate to http://localhost:8080/ to see it in action. I suggest using the demo as a playground to familiarize yourself with the API. The code inside of demo/src/lib is the exact code that lives in src so feel free to play around with the source code.

Motivation

This was built in response to the twitter discussion started by @DkDodo(Dominik). In his tweet, Dominik shared a screenshot showing how the new React.use api will automatically memoize a selected portion of a context value likeso:

import { createContext, use, useMemo } from "react";

const MyContext = createContext({
  foo: "",
  bar: 0,
});

// Will only rerender if `foo` changes
const useFoo = () => {
  return useMemo(() => {
    const { foo } = use(MyContext);

    return foo;
  }, []);
};

Upon seeing this tweet, I sloppily threw together an implementation of an API that the React community has long asked for, the React.useContextSelector, and asked Dominik if it made sense:

const useContextSelector = (ctx, selector) => {
  return useMemo(() => {
    const value = use(ctx);
    return selector(value);
  }, []);
};

Dominik was quick to point out that I should have added the selector to the dependency array (oops!) and that, even so, this implementation might fare worse than his original example since the caller of useContextSelector must memoize the selector function they provide as an argument.

Then things started to get really interesting when Dan Abramov chimed in with the following quote, "the whole point of this is to not have selectors btw. selectors don’t compose." He then included the following code sample:

useTheme() {
  return useContext(AppContext).theme
}

useColor() {
  return useTheme().color
}

along with the following quote, "if this was done with context selectors then you have to know precisely what you’re selecting in advance. the author of useColor can’t “narrow down” the scope to just the color changes"

But something still didn't feel quite right about doing away with the useSelector pattern altogether. So over the next couple of days I designed @interbolt/selectable to experiment with a way to somewhat "fix" useContextSelector's composability problem.

Blog

I plan to write about this in more depth via a blog post in the coming days. Until then stay tuned via RSS - https://interbolt.org/rss.xml.