Nanobot: Polygonal Mascot

    react-quarks
    TypeScript icon, indicating that this package has built-in type declarations

    2.0.1 • Public • Published

    React Quarks

    React Quarks is a simple, lightweight and easy to use state management library for React with full support for Asynchronous State Updates

    Table of Contents

    1. Installation
    2. Basics
    3. Asynchronous Updates
      1. Canceling pending updates
    4. Readonly quark state typings
      1. Example Without Read-only
      2. Example With Read-only
    5. Limitations
      1. Function as state value
      2. Promise as state value
    6. Dictionaries, Arrays, Selectors, Actions and Middlewares
      1. Selectors
      2. Selectors with arguments
      3. Actions
      4. Side Effects
      5. Subscription
      6. Middlewares
        1. Creating a Middleware
        2. Global Middleware
        3. Included Middlewares
          1. Immer Middleware
          2. Catch Middleware
          3. Debug History Middleware
    7. SSR

    Installation

    npm install react-quarks

    OR

    yarn add react-quarks

    Basics

    Import the quark method from the react-quarks package.

    import { quark } from "react-quarks";

    Create a new Quark

    const counter = quark(0);

    To declare the quark type you will need to assert the desired type into the initial value (This is a limitation of TypeScript and how the generic types inference works, if you were to specify the type with the <> symbols you'd have to also manually define the types for all of the quark selectors, actions and middlewares as well, no inference):

    const listOfNumbers = quark([] as number[]);
    // OR
    const initialValue: number[] = [];
    const listOfNumbers = quark(initialValue);

    Access data in the Quark outside React

    const currentCounterValue = counter.get();

    Update the state of the Quark outside React

    With a simple .set(value)

    counter.set(counter.get() + 1);

    With a dispatch function

    counter.set((currentState) => currentState + 1);

    With a promise

    counter.set(Promise.resolve(counter.get() + 1));

    With a dispatch function returning a promise

    counter.set((currentState) => Promise.resolve(currentState + 1));

    Use the Quark within a React functional component

    const MyComponent: React.FC = () => {
      const counterState = counter.use();
    
      const incrementCounter = () => {
        counterState.set((state) => state + 1);
      };
    
      return (
        <div>
          <p>Current count: {counterState.value}</p>
    
          <button onClick={incrementCounter}>Increment</button>
        </div>
      );
    };

    Asynchronous Updates

    Quarks can accept asynchronous updates out of the box.

    To perform an asynchronous state update simply pass a Promise object to the set method like this:

    const counter = quark(0);
    
    // ...
    
    counter.set(Promise.resolve(1));

    or a function that returns a Promise:

    const counter = quark(0);
    
    // ...
    
    counter.set(() => Promise.resolve(1));

    All asynchronous updates are tracked by the Quark instance, and any state update will cancel all the previous non-resolved async updates. With this it's assured that no race-conditions shall occur when using Quarks async updates.

    Canceling pending updates

    In case an asynchronous (or synchronous) update that has been dispatched cannot be finished it can be cancelled by throwing a special class CancelUpdate.

    import { CancelUpdate } from "react-quarks";
    
    const data = quark(
      {
        /* some initial data */
      },
      {
        actions: {
          async updateWithNewData() {
            try {
              // We request new data from the server
              const result = await fetchNewData();
              // Request was successful, update the state with the result
              return result;
            } catch (e) {
              // Request failed, update is cancelled
              throw new CancelUpdate();
            }
          },
        },
      }
    );

    When an action function throws, the quark state update does not happen regardless of what has been thrown, however if the thrown value is not an CancelUpdate instance, that event will be logged to the console as an error and propagated up to the initial caller.

    Readonly quark state typings

    It is possible to make Quark states have a readonly type when accessed via .use().value or .get() or a selector. This can help avoid errors that may occur when changing properties of a state, since by design states of Quarks (and React states) are intended to be immutable.

    You can enable this feature by extending the global Quarks.TypeConfig interface with a ENABLE_READONLY_STATES property:

    declare global {
      namespace Quarks {
        interface TypeConfig {
          ENABLE_READONLY_STATES: true;
        }
      }
    }

    Example Without Read-only

    const myQuark = quark({ foo: { bar: 0 } });
    const value = myQuark.get(); // const value: { foo: { bar: number } }
    
    value.foo.bar = 1; // OK, no errors

    Example With Read-only

    const myQuark = quark({ foo: { bar: 0 } });
    const value = myQuark.get(); // const value: { readonly foo: { readonly bar: number } }
    
    value.foo.bar = 1; // Error: Cannot assign to 'bar' because it is a read-only property.

    Limitations

    It's impossible to assign a Function or a Promise object as the Quark value, since any functions or promises will automatically be "unpacked" by the Quark set() function (even if they are nested, i.e. () => () => void).

    If you must have a promise or a function as the value of the Quark then wrap it within a object like so:

    Function as state value

    const quarkWithAFunction = quark({ fn: () => {} });
    
    const someFunction () => {
        // ...
    };
    
    quarkWithAFunction.set({ fn: someFunction });

    Promise as state value

    const quarkWithAPromise = quark({ p: Promise.resolve() });
    
    const somePromise = new Promise((resolve) => {
      // ...
    });
    
    quarkWithAPromise.set({ p: somePromise });

    Dictionaries, Arrays, Selectors, Actions and Middlewares

    If your Quark holds arrays of data or dictionaries using just use, get and set can become cumbersome, to make it easier to use your Quarks you can define actions, selectors and middlewares that will help minimize the boilerplate and ease the development.

    Selectors

    Often subscribing to the whole dictionary or array may not be something you want, take for example this Quark:

    const siteSettings = quark({
      title: "My Webpage",
      theme: "dark",
    });

    Here both title and theme are stored in the same Quark, this means that if we change the value of the title all component that "use" this Quark will update, even if all they actually use is the theme property (in which case the update is unnecessary). To solve this issue we can use selectors.

    Selectors can be used in two ways

    1. via the builtin hook useSelector
    2. or by adding custom selector to the Quark definition
    useSelector
    const selectTitle = (state: QuarkType<typeof siteSettings>) => state.title;
    
    const PageHeader: React.FC = () => {
      const title = siteSettings.useSelector(selectTitle);
    
      return <h1>{title}</h1>;
    };

    Warning! Do not use inline functions as selectors (ie. useSelector((state) => state.title))), selector is a dependency and whenever it changes a rerender will happen, passing a inline function will cause a endless loop of rerenders. For selectors always use functions declared outside react tree like shown in the example or wrap them in a React's useCallback().

    custom selector

    First we will need to change how the Quark is defined:

    const siteSettings = quark(
      {
        title: "My Webpage",
        theme: "dark",
      },
      {
        selectors: {
          useTitle(state) {
            return state.title;
          },
        },
      }
    );

    And with this the useTitle hook will be added to our Quark.

    const PageHeader: React.FC = () => {
      const title = siteSettings.useTitle();
    
      return <h1>{title}</h1>;
    };

    Both of the above solution achieve the same thing - the PageHeader component will re-render whenever the title changes but not when the theme changes.

    It's worth mentioning that selectors can be used to do much more than that, the returned value from the selector doesn't have to be a property of the Quark state, it can be anything, and the selector will cause the component to update only when that returned value is different from the previous one.

    Selectors with arguments

    Selectors can take custom arguments too. It can be done by simply adding arguments to the selector function after the Quark State, like so

    const siteSettings = quark(
      {
        title: "My Webpage",
        theme: "dark",
      },
      {
        selectors: {
          useTitleLetter(state, letterIndex: number) {
            return state.title[letterIndex];
          },
        },
      }
    );

    and then,

    const PageHeader: React.FC = () => {
      const letter = siteSettings.useTitleLetter(3);
    
      return <h1>Third letter of the title is: {letter}</h1>;
    };

    Actions

    When the Quark holds an array or a dictionary updating the state with the set method may create a boilerplate, for this reason you might want to create a helper functions that will contain all of the repeatable logic in one place. With Actions you can more tightly integrate those helpers with the Quark.

    Let's again consider the above siteSettings example. If you'd want to update the title you'd need to do something like this:

    siteSettings.set((state) => ({ ...state, title: "My new website title" }));

    All of this for changing one property, and it would only get worse if that property was deeply nested within the siteSettings.

    To avoid this unnecessary boilerplate you can add actions to the Quark object:

    const siteSettings = quark(
      {
        title: "My Webpage",
        theme: "dark",
      },
      {
        actions: {
          setTitle(state, newTitle: string) {
            return { ...state, title: newTitle };
          },
          // async actions are also possible
          async setThemeAfter1sec(state, theme: string) {
            await new Promise((r) => setTimeout(r, 1000));
            return { ...state, theme };
          },
        },
      }
    );

    And with that the actions can be used like so:

    siteSettings.setTitle("My new website title");
    siteSettings.setThemeAfter1sec("light");

    Actions always take the Quark state as it's first argument, and optionally some other following arguments. The method exposed by the Quark will be called the same as the one in the Quark definition but without the first argument with Quark state.

    Side Effects

    Sometimes you may want something to happen when the Quark state changes. Quark effects allow for just that.

    For example let's say we have a Quark that holds some information that we expect will often change throughout the life of our app and we want to have a timestamp of every change to that information. Instead of adding a new 'Date.now()' on every set call we can add an effect like so:

    const data = quark(
      {
        myData: "some data",
        lastUpdated: 0,
      },
      {
        sideEffect: (prevState, newState, set) => {
          if (prevState.myData !== newState.myData) {
            set({ ...newState, lastUpdated: Date.now() });
          }
        },
      }
    );

    Now whenever we change the myData property to a different value myDataEffect will fire and set the lastUpdated property to the current time.

    Effect methods take three argument:

    • first is the previous state of the Quark
    • second is the new or rather current state of the Quark
    • third is a set function that allows for updating the Quark state

    Subscription

    Quarks can also be subscribed to outside of React.

    Example
    const counter = quark(0);
    
    const subscription = counter.subscribe(
      (currentState: number, cancel: () => void) => {
        // run on Quark state change
      }
    );
    
    subscription.cancel(); // Cancels the subscription (equivalent of removeListener in event emitter's)

    subscribe() method takes a callback as it's argument that's invoked whenever the Quark state changes. That callback can take up to two arguments, the current state of the Quark and a method for cancelling the subscription from within the callback. Subscription can also be cancelled via a method returned returned by the subscribe() as shown in the example.

    Middlewares

    Middlewares give you the ability to intercept any state updates and modify or prevent them from occurring as well as observe actions sent to the Quarks.

    A middleware ought to be a function taking up to 5 arguments:

    • arg_0 - function getState(): T - method which returns the current Quark state value
    • arg_1 - action: SetStateAction<T, M> - dispatched value, this is the same as what is provided to the set() function argument
    • arg_2 - function resume(v: SetStateAction<T, M>): void - this method will resume the update flow, value provided to it will be forwarded to the next middleware
    • arg_3 - function set(v: SetStateAction<T, M>): void - this method allows to break out from the update flow and set the state immediately bypassing any following middlewares
    • arg_4 - updateType: 'sync' | 'async' - this argument indicates if the source of the update was synchronous or asynchronous, whatever is provided to the set() method will always have a sync type at first, then if the passed value is a Promise or a function returning a Promise, the resolved value from that Promise will be given to the middleware with a type of async.
    Example

    A naive middleware that will catch any errors thrown by an action.

    const catchMiddleware: QuarkMiddleware<number, never> = (
      getState,
      action,
      resume
    ) => {
      try {
        resume(action);
      } catch (e) {
        console.error("An error occurred during state update!");
      }
    };
    
    const counter = quark(0, { middlewares: [catchMiddleware] });
    
    // The action in this case will be a function
    counter.set(() => {
      throw new Error();
    }); // Output: 'An error occurred during state update!'

    Creating a Middleware

    One of the uses of middleware can be to extend the functionality of a Quark.

    As an example, imagine you have a Quark storing a number, but you'd want to be able to set() it's state with a string. The goal is to have the Quark contain always a numeric value, but the set() function to accept both numbers and strings, and not only that, but the action passed to the set() could also be a Promise resolving any of the two, or a callback that returns either.

    This can be easily achieved with a right middleware.

    Example
    const numParserMiddleware: QuarkMiddleware<number, string> = (
      getState,
      action,
      resume
    ) => {
      // If the action is a string, rather than a number, parse it to a number and continue the update with it
      if (typeof action === "string") {
        const num = Number(action);
    
        if (isNaN(num)) throw new Error("This string is not parsable to a number!");
    
        return resume(num);
      }
    
      // otherwise, continue with the original action
      resume(action);
    };
    
    const counter = quark(0, { middlewares: [numParserMiddleware] });
    
    counter.set("2"); // OK
    counter.get(); // 2 as a number type
    
    counter.set(() => "123"); // OK
    counter.get(); // 123 as a number type
    
    counter.set(() => Promise.resolve("777")); // OK
    // ...
    counter.get(); // 777 as a number type

    If you want the middleware to play nicely with TypeScript don't forget to add the QuarkMiddleware type to your middleware (as shown in the example). QuarkMiddleware is a type that takes in two generics, first is the type of the Quark (as in the type that the get() method should return), and the other is a type that is extending the Quark action (ie. the type that can be now additionally accepted in the set() method aside from the actual Quark type).

    Global Middleware

    It is also possible to add a global middleware. Global middlewares will be automatically added to all quarks.

    To add a global middleware use the addGlobalQuarkMiddleware function, or to overwrite all the global middlewares setGlobalQuarkMiddlewares. You can also lookup all the current global middlewares with getGlobalQuarkMiddlewares.

    Included Middlewares

    react-quarks library includes a few Middleware factories to use out of the box.

    • Immer Middleware - extends the standard function setters of quarks with the immer library to allow for updating state by detecting changes made on the current state provided to that functions. When this middleware is used it's possible to update quarks by mutating state properties directly, when within action methods or set functions (ex. quark.set(current => { current.foo = newValue; return current; })))
    • Catch Middleware - a middleware that provides you a way for catching errors thrown by the set state action callbacks and promises.
    • Debug History Middleware - a middleware that provides you a way of tracking the Quark update actions and the current state of the Quark.
    Immer Middleware
    import {
      quark,
      createImmerMiddleware,
      addGlobalQuarkMiddleware,
    } from "react-quarks";
    
    addGlobalQuarkMiddleware(createImmerMiddleware({ mapAndSetSupport: true }));
    
    const state = quark(
      { value: "foo" },
      {
        actions: {
          changeValue(currentState, setTo: string) {
            currentState.value = setTo;
            return currentState;
          },
        },
      }
    );
    
    state.changeValue("bar");
    state.get(); // => { value: "bar" }
    
    // Works even with async updates
    
    state.set(async (currentState) => {
      currentState.value = await fetch(/* ... */);
      return currentState;
    });
    Catch Middleware
    import { quark, createCatchMiddleware } from "react-quarks";
    
    const catchErrorMiddleware = createCatchMiddleware({
      catch: (e) => {
        console.log("An error has been caught!", e);
        // handle the error
      },
    });
    
    const counter = quark(0, {
      middlewares: [catchErrorMiddleware],
    });
    
    counter.set(() => {
      throw new Error("Update Failed!");
    });
    // Output: 'An error has been caught! Error: Update Failed!'

    It is recommended for this middleware to be the very first middleware provided in the middlewares array.

    Debug History Middleware
    import { quark, createDebugHistoryMiddleware } from "react-quarks";
    
    const counterDebugMiddleware = createDebugHistoryMiddleware({
      name: "Counter Quark",
      trace: false, // When enabled a stack trace will be generated for each dispatched set state action
      realTimeLogging: true, // When enabled each set state action will be logged to the console in real time
      useTablePrint: false, // When enabled the history will be printed to the console with a `console.table()`, otherwise `console.log()` will be used
    });
    
    const counter = quark(0, {
      middlewares: [counterDebugMiddleware],
    });

    It is recommended for this middleware to be the last middleware provided in the middlewares array.

    When this middleware is used a method in the global scope will be created that will allow the developer to view the quark update history via console.

    To show the history, open the console and invoke the method: printQuarkHistory()

    By default a history of all quarks with this middleware will be logged to the console. You can filter out which Quark is to be shown and how many history entries are to be displayed by specifying the options argument:

    printQuarkHistory({
      name: "Counter Quark", // Name of the specific Quark to show history of. Default is show all
      showLast: 10, // Number of set state action's history entries to show. Default is 16
      useTablePrint: true, // When enabled the history will be printed to the console with a `console.table()`, otherwise `console.log()` will be used
    });

    SSR

    To support Server Side Rendering, Quarks provide a way to serialize them (on the server) and then hydrate (on the client). Each quark that is going to be serialized must have a unique name.

    Fist you will need to install serialize-javascript on the backend. This package is required for the serializeQuarks() function to work.

    npm i serialize-javascript

    OR

    yarn add serialize-javascript

    Then you can use it as follows:

    // Server Side
    import Express from "express";
    import { renderToString } from "react-dom/server";
    import { quark, serializeQuarks } from "react-quarks";
    
    const serverHeader = quark("Hello World", { name: "header" });
    
    const Home = () => {
      const header = serverHeader.use();
    
      return <h1>{header.value}</h1>;
    };
    
    const app = Express();
    
    app.get("/", (req, resp) => {
      const html = renderToString(<Home />);
      const serialized = serializeQuarks();
    
      return `
        <html>
          <head>
            <script>
              window.__PRELOADED_STATE__ = ${serialized}; // quotation marks are already included
            </script>
          </head>
          <body>
            <div id="root">
              ${html}
            <div>
          </body>
        </html>
      `;
    });
    
    app.listen(80);
    
    // Client side
    import ReactDOM from "react-dom";
    import { quark, hydrateQuarks } from "react-quarks";
    
    const clientHeader = quark("", { name: "header" }); // the same name as in the `serverHeader`
    
    const Home = () => {
      const header = clientHeader.use();
    
      return <h1>{header.value}</h1>;
    };
    
    if (window.__PRELOADED_STATE__) {
      hydrateQuarks(window.__PRELOADED_STATE__);
    }
    
    // Serialized data is not needed after hydration
    delete window.__PRELOADED_STATE__;
    
    ReactDOM.hydrate(<Home />, document.getElementById("root"));

    Install

    npm i react-quarks

    DownloadsWeekly Downloads

    223

    Version

    2.0.1

    License

    MIT

    Unpacked Size

    89.9 kB

    Total Files

    94

    Last publish

    Collaborators

    • ncpa0cpl