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

active-store

v0.3.12

Published

Active Store is a new state library for React that heavily utilizes the React Suspense API.

Downloads

133

Readme

Active Store is a new state library for React that heavily utilizes the React Suspense API.

Quick start

To get started install the npm package with npm i active-store. You can then create a store as shown below:

import { createContext, useContext } from "react";
import { activeState, activeQuery, activeComputed } from "active-store";

function activeAppStore() {
  // activeState is the simplest building block of active store.
  // It keeps a piece of state and updates UI when the state changes
  // You can later access run:
  // - userLogin.get() to get the current value of the active state
  // - userLogin.set("new-value") to change the value stored in states
  const userLogin = activeState("ziolko");

  // activeQuery is like useQuery in react-query - it fetches data
  // for every unique set of arguments. In the case below there's only
  // one argument called - login.
  const githubProfile = activeQuery(
    (
      login: string
    ): Promise<{
      id: number;
      login: string;
      avatar_url: string;
      name: string;
    }> =>
      fetch(`https://api.github.com/users/${encodeURIComponent(login)}`).then(
        (x) => x.json()
      )
  );

  // activeComputed allows to get a computed value based on
  // activeState and activeQuery. It uses React Suspense api to wait
  // for activeQuery to resolve
  const profile = activeComputed(() => {
    // get currently selected github login. You will see a lot of `.get()`
    // in the databases using active-store
    const login = userLogin.get();

    // start fetching data from activeQuery for currently
    // selected github login. The line below will suspend until
    // github profile finishes loading.
    // There's no need for special handling of the async loading state.
    return githubProfile.get(login);
  });

  // Of course you can combine computed values in activeComputed, too
  const userName = activeComputed(() => profile.get().name);

  return {
    userLogin,
    profile,
    userName,
  };
}

const store = activeAppStore(); // Create an instance of the store

// Define a helper hook 'useStore' to get an instance
// of the store in your components
const storeContext = createContext<ReturnType<typeof activeAppStore>>(store);
export const useStore = () => useContext(storeContext);

You are now all set to use the store in your app:

import "./App.css";
import { useStore } from "./store";
import { useActive, ActiveBoundary } from "active-store";

export default function App() {
  return (
    <div>
      <CurrentUserPicker />
      {/*  
        Active boundary wraps a section of the app that loads and fails
        together 
        - fallback is used while data required to load child components
      is loading. 
        - errorFallback is used when any of the data fails to load
      with an exception
      */}
      <ActiveBoundary
        fallback="Loading..."
        errorFallback="There was an error while loading the data :("
      >
        <GithubProfile />
      </ActiveBoundary>
    </div>
  );
}

function CurrentUserPicker() {
  const store = useStore();
  const currentLogin = useActive(store.userLogin);
  const options = ["stephencelis", "lidel", "arogozhnikov", "ziolko"];
  return (
    <>
      {options.map((login) => (
        <button
          key={login}
          onClick={() => store.userLogin.set(login)}
          style={{ background: currentLogin === login ? "red" : "transparent" }}
        >
          {login}
        </button>
      ))}
    </>
  );
}

function GithubProfile() {
  const store = useStore();

  // React will suspend until the data is loaded
  const profile = useActive(store.profile);

  // React will suspend until the data is loaded
  const name = useActive(store.userName);

  return (
    <div>
      {name} <img src={profile.avatar_url} width={80} />
    </div>
  );
}

Examples

Reference

activeState

The simplest building block of the app state. It's like the useState hook.

const state = activeState<T>(initialState: T, options?: { 
  // onSubscribe is called when first subscriber subscribes to the state. 
  // The callback returned from onSubscribe is called when last subscriber unsubscribes
  onSubscribe?: () => () => void;
  // Number of miliseconds the data will be cached after 
  // last subscriber unsubscribes (defaults to infinity)
  gcTime?: number;
});


// Returns the current value of the state
state.get();

// Sets new value for the state. Trigers re-renders of
// components that depend on it. Triggers invalidation
// on activeComputed that depend on it.
state.set(newValue: T);

// Manually subscribes to changes in the state.
// Takes a listener as a parameter. Returns unsubscribe function.
state.subscribe(listener: () => any) => () => void;

Alternatively, you can provide a factory function as an initialState:

const greetings = activeState((name: string) => `Hello ${name}`, {
  onSubscribe(name: string) {
    console.log(`Subscribed to ${name}`);
    return () => { console.log(`Unsubscribed from ${name}`); }
  },
});

console.log(greetings.get('Adam')); // will print "Hello Adam"

// Set greetings for "Adam" to "Hi Adam"
// Notice that the value comes first, and key comes after it
greetings.set('Hi Adam', 'Adam');

activeQuery

This is heavily based on React Query, so if you are familiar with this library you will feel like home.

// Create a query with a factor function returning promise.
// Example: https://stackblitz.com/edit/vitejs-vite-mosens?file=src%2Fstore.ts
// Important: The query re-fetches based only on the provided parameters.
// If you use e.g. activeState in the factory function, updating it's state
// won't trigger a re-fetch.
const query = activeQuery(factory: (...args: P) => Promise<R>, options?: { 
  // Number of retries in case of failure
  retry?: number | false;
  // onSubscribe is called when first subscriber subscribes to the state. 
  // The callback returned from onSubscribe is called when last subscriber unsubscribes
  onSubscribe?: (...args: P) => () => void;
  // Number of miliseconds the data will be cached after 
  // last subscriber unsubscribes (defaults to infinity)
  gcTime?: number;
});



// If the query for "hello" "world" has already resolved, returns the value.
// If it rejected it throws the rejection reason as an exception
// If query is pending, it throws a React Suspense error that
// suspenses rendering React components and activeComputed
// (more on this below).
query.get("hello", "world");

// Returns the current state of the query. The returned state
// has the following fields that are very similar to react-query
// - status: "pending" | "success" | "error";
// - isPending: boolean;
// - isSuccess: boolean;
// - isError: boolean;
// - isRefetching: boolean;
// - isFetching: boolean;
// - isStale: boolean;
// - data?: R;
// - error?: any;
// - dataUpdatedAt?: number;
// - errorUpdatedAt?: number;
// - fetchStatus: "fetching" | "paused" | "idle";
query.state("hello", "world");

// Returns a promise for query for given parameters
query.getAsync("hello" ,"world");

// Invalidate query for given parameters - will mark data as stale
// and refetch if any component uses the query (either directly, or
// through activeComputed)
query.invalidateOne("hello", "world");

// Invalidate query for all entries for which selector returns true.
// This marks data as stale and refetch queries used in any visible
// React component (either directly or through activeComputed)
// Options:
// - reset (default false) - reset the query to the initial state
//                           (idle, with no data or error)
query.invalidate(
  selector: (...args: P) => boolean,
  options?: { reset?: boolean }
);

activeComputed

Creates a computed state based on any other active state. If any React component is subscribed to it, it recomputes automatically whenever any of its dependency changes.

// The provided factory function must not be async
// (or return a Promise) - TypeScript will complain when this happens.
// You can use any combination of activeState, activeQuery, or
// activeComputed inside.
const computed = activeComputed(factory: (...args: P) => R, options?: { 
  // Number of miliseconds the data will be cached after 
  // last subscriber unsubscribes (defaults to infinity)
  gcTime?: number;
});

// As active computed can depend on active query, it has to
// follow its async semantics:
// - If the computed for "hello" "world" has already resolved,
//   returns the value.
// - If it rejected it throws the rejection reason
//   as an exception
// - If it's pending, it throws a React Suspense error that
//   suspends rendering React components and activeComputed
//   that depend on it (more on this below).
computed.get("hello", "world");

// Returns a promise for computed for given parameters
computed.getAsync("hello", "world");


// Returns the current state of the computed value.
// The returned state has the following fields:
// - status: "pending" | "success" | "error";
// - data?
// - error?
computed.state("hello", "world");

useActive

It's like useSelector from Redux. It connects your components with store.

// Subscribes to the value of the activeComputed property.
// Every time the value changes, component is re-rendered.
const value = useActive(() => store.computed.get(userId));

// If `get` doesn't take any parameters, you can just pass
// a reference to the getter:
const value = useActive(store.currentUser.get);

// For convenience you can skip the `.get` part and
// `useActive` will call it automatically:
const value = useActive(store.currentUser);

getActive

Compute value of an expression. If any active query or active computed is pending, it will wait until it fully resolves

const query = activeQuery(
  () => new Promise((res) => setTimeout(() => res("Hello"), 1000))
);

// Will await 1s until query resolves. 
// Returned value will be equal "Hello World"
const value = await getActive(() => `${query.get()} World` ); 

ActiveBoundary

Active boundary wraps a section of the app that loads and fails together. It's basically <Suspense> and React error boundary in a single component.

  • fallback is used while data required to load child components is loading.
  • errorFallback is used when any of the data fails to load with an exception

You can find an example of using it in the "Quick start" section.

How does suspending activeQuery and activeComputed work

This library uses the React Suspense API for handling loading state. Under the hood it (ab)uses exceptions.

When activeComputed tries to get value of an active query (e.g. user.get('mateusz')) that is currently in a pending state, an special kind of exception is thrown. The trick is that the exception is also a Promise.

When the promise resolves, React re-renders the component so activeComputed recomputes the value and this time the query is already resolved so it successfully computes the value.

License

The project is licensed under the MIT permissive license