pullstate

repository·master·Indexed 22 days ago

https://github.com/lostpebble/pullstate

A lightweight (approx. 7KB) state management library for React that uses immer and hooks for performant state retrieval. It enables global, decoupled state management via stores that can be imported anywhere, eliminating the need for complex provider nesting. Features include selection functions for optimized re-renders, Async Action management with caching, and specialized hooks like useWatch and useBeckon for handling asynchronous logic.

Tokens
24.2K
Snippets
84
Records
109
Agent score
77%

What's inside pullstate

  1. How pullstate state management works

    master

    Pullstate allows you to decouple state from the React component tree. Instead of passing state down through props (prop drilling), you define stores in separate files and import them directly into any component that needs them.

    Components 'pull' the state they need using useState() hooks, and any part of the application can 'push' updates by calling update() on the store. This creates an 'omnipresent' state that is accessible globally without requiring a React Context provider at the root of your tree.

  2. How Async hooks work in Async Actions

    master

    When creating an Async Action using createAsyncAction, you can provide a second argument containing a hooks object. This object allows you to intercept the action lifecycle at three specific stages: before the action runs (short-circuiting), after the action completes (post-action), or when deciding whether to use a cached result (cache-breaking).

    In server-rendering environments using a centralized Pullstate "Core" and <PullstateProvider>, the stores object is provided within the hook arguments. For client-side only applications, you should import and use your stores directly instead of relying on the stores argument.

    const searchPicturesForTag = createAsyncAction(
      async ({ tag }) => {
        // action code
      },
      {
        postActionHook,
        shortCircuitHook,
        cacheBreakHook
      }
    );
  3. What are Async Actions in pullstate?

    master

    Async Actions are a feature in pullstate designed to handle asynchronous state transitions (like API calls) without cluttering your stores with manual loading and error state variables (e.g., userLoading, userLoadError).

    They solve two primary problems:

    1. Boilerplate Reduction: They allow views to naturally listen for and initiate asynchronous state, removing the need for manual lifecycle management in componentDidMount() or useEffect() hooks.
    2. Server-Side Rendering (SSR) Support: They provide a mechanism to resolve an application's asynchronous state before rendering to the user, avoiding the verbose manual try/catch patterns typically required to pre-fetch data and update stores during SSR.
  4. How Async Action caching works

    master

    Pullstate automatically caches the results of Async Actions based on a "fingerprint" generated from the arguments passed to the action.

    • Fingerprint: An internal representation of the arguments. If you call an action with { tag: 'dog' }, the result is cached for that specific object.
    • Behavior: If the action is called again with the exact same arguments, Pullstate returns the cached result instead of re-running the asynchronous function.
    • Best Practice: Define your actions with as many arguments as necessary to uniquely identify the specific action instance, but keep them as brief as possible.

    Warning: If you use an Async Action to update a Pullstate store (e.g., calling Store.update() inside the action), the store will not be updated when the action returns a cached result. To handle side effects that must run every time an action is triggered (even if cached), use postActionHook().

  5. How omnipresent state works in Pullstate

    master
    Pullstate allows you to decouple state from the React component tree. Because stores are exported as standard JavaScript/TypeScript constants, you can import them and call .update() from anywhere in your application (outside of components) without needing to pass props or use Context. Components then react to these changes by using the .useState() hook.
  6. Enable Universal Fetching for Async State

    master

    Any action using useBeckon() in the current render tree can have its state resolved on the server before rendering to the client. This enables dynamic page generation.

    Requirement: To work correctly, your data-fetching functions must be "isomorphic" or "universal" (capable of running on both the server and the client), such as those provided by Apollo Client or Wildcard API.

    // The action must be isomorphic
    const searchPicturesForTag = PullstateCore.createAsyncAction(async ({ tag }) => {
      const result = await PictureApi.searchWithTag(tag);
      // ... handle result
    });
    
    // In the component, useBeckon will be pre-resolved on the client after SSR
    export const PictureExample = (props: { tag: string }) => {
      const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });
    
      if (!finished) return <div />;
      // ...
    };
  7. Use draft vs original in reactions for performance

    master

    When writing a reaction function, you receive both a draft and an original object. To ensure optimal performance, follow these rules:

    • To mutate/change state: Use the draft object. This uses Immer's proxy mechanism to track changes.
    • To read/reference state: Use the original object. Referencing values directly on the draft object can incur a performance penalty due to the internal workings of Immer's JavaScript proxies. Using original provides a plain object of your state for faster read operations.
  8. Use `postActionHook` to synchronize Async Action results with stores

    master

    When creating an Async Action, avoid updating state stores directly inside the action function. If an action hits a cached value, the action body will not re-run, causing your stores to become out of sync with the cached data.

    Instead, use the postActionHook option. This hook is guaranteed to run after every action completion, regardless of whether the action was executed fresh or retrieved from the cache. This ensures your application state (e.g., view data, organized results) remains consistent.

    Note on stores: The stores object is only available in the hook if you are using <PullstateProvider> for server-side rendering. For client-side only applications, you should import and update your stores directly within the hook.

    const searchPicturesForTag = PullstateCore.createAsyncAction(
      async ({ tag }) => {
        const result = await PictureApi.searchWithTag(tag);
    
        if (result.success) {
          return successResult(result);
        }
    
        return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
      },
      {
        postActionHook: ({ result, stores }) => {
          if (!result.error) {
            // For SSR, use stores. For client-side, import your store directly.
            stores.GalleryStore.update(s => {
              s.pictures = result.payload.pictures;
            });
          }
        },
      }
    );
  9. Set up pullstate for server-rendering

    master

    When using server-rendering, you must create a central reference for all your stores to ensure consistency. This is achieved by using createPullstateCore(), passing an object that contains all your instantiated stores. This core instance acts as the central registry for your application's state in a server-rendered environment.

    // 1. Define and instantiate your stores
    import { Store } from "pullstate";
    
    interface IUIStore {
      isDarkMode: boolean;
    }
    
    export const UIStore = new Store<IUIStore>({
      isDarkMode: true,
    });
    
    // 2. Create the PullstateCore to centralize stores for server-rendering
    import { createPullstateCore } from "pullstate";
    
    export const PullstateCore = createPullstateCore({
      UIStore
    });
  10. Integrate Pullstate with Redux Devtools

    master

    You can monitor Pullstate stores using the Redux Devtools browser extension. To enable this, use the registerInDevtools function and pass an object containing the stores you wish to monitor. This registration should be performed after your Store definitions have been initialized.

    Once registered, each store will appear in the Redux Devtools tab, allowing you to inspect state changes and history.

    import { registerInDevtools, Store } from "pullstate";
    
    // Store definition
    const ExampleStore = new Store({
      //...
    });
    
    // Register as many or as few Stores as you would like to monitor in the devtools
    registerInDevtools({
      ExampleStore,
    });
  11. Edit existing documentation or blog posts

    master

    To modify existing content:

    • Docs: Navigate to the docs/ directory and edit the corresponding .md file.
    • Blog Posts: Navigate to the website/blog/ directory and edit the corresponding .md file.

    Ensure you preserve the frontmatter block (containing id and title) at the top of the file to maintain correct routing and display.