1.0.12 • Published 7 months ago

whoosh-react v1.0.12

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

Whoosh - minimalistic React state manager

Whoosh is a React state manager which entire API consists of exactly one function - createShared().

TL;DR version of the docs

Navigation

Mindset

Whoosh aims to be

  • easy to use and reason about,
  • general and extendable,
  • compact*.

*Whoosh is very small. Its entire source code is under 80 lines (code readability is not sacrificed) and it takes less than 1 Kbyte in minimized form.

Installation

npm install --save whoosh-react

Examples

Counter

This example on codesandbox.io

  1. Create Shared State
// AppState.ts
import { createShared } from 'whoosh-react';

export const appCounter = createShared<number>(0);

createShared() accepts an initial value and returns an object that represents Shared State.

  1. Use Shared State in React components
// Counter.tsx
import { appCounter } from './AppState.ts';

const CounterValue = () => {
    const counter = appCounter.use();
    return <p> { counter } </p>;
};

const CounterControls = () => {
    const reset = () => appCounter.set(0);
    const addOne = () => appCounter.set(previousValue => previousValue + 1);
    return (<>
        <button onClick={reset} > Reset </button>
        <button onClick={addOne} > Add 1 </button>
    </>);
};

In this example we call two function from the appCounter:

  • use() returns current value of the Shared State. It is a React Hook that will trigger component re-render every time Shared State changes.

  • set() is a plain JS function that updates Shared State. It accepts either a new value or a function that returns the new value.

  1. Render the components. They can be anywhere in the tree.
const RootComponent = () => (
    <>
        <A>
            <CounterValue/>
        </A>
        <B>
            <CounterControls/>
        </B>
    </>
);

Counter with Reducer

This example on codesandbox.io

createShared() has the second optional parameter which is a Reducer function.

Reducer is a function of type (previousValue: S, input: A) => S. It describes how Shared State of type S should be modified based on the previousValue and the input. input is the value that was passed to the appCounter.set(). Notice, that if an invalid input is passed to set() a Error will be thrown to the caller of set().

// AppState.ts
export const appCounter = createShared<number, { operation: 'add' | 'subtract' | 'set'; arg: number; }>(
    0,
    (previousValue, { operation, arg }) => {
        switch(operation) {
            case 'add': return previousValue + arg;
            case 'subtract': return previousValue - arg;
            case 'set': return arg;
        }
        // This Error will be thrown to the caller of `appCounter.set(__invalid_parameter__)`
        throw new Error(`appCounter Reducer: operation ${operation} is not supported!`)
    }
);
// Counter.tsx
const CounterControls = () => {
    const reset = () => appCounter.set({operation: 'set', arg: 0});
    const addOne = () => appCounter.set({operation: 'add', arg: 1});
    return (<>
        <button onClick={reset} > Reset </button>
        <button onClick={addOne} > Add 1 </button>
    </>);
};

Passing a function to appCounter.set() is still possible:

const toggleBetween0and1 = () => appCounter.set(
    previousValue => ({
        operation: (previousValue > 0? 'subtract' : 'add'),
        arg: 1
    })
);

Refer to this tutorial on advanced reducer usage in Whoosh.

Most common reducers are available (but completely optional) in the Reducer library

Shared State object API

createShared() returns a Shared State object with the next interface

// S - State type
// A - Reducer input type (if Reducer is present)

interface SharedState<S, A = S> {
    use(): S;                                   // React Hook that returns current state value
    get(): S;                                   // Getter
    set(a: A | ((s: S) => A)): void;            // Setter / Dispatcher
    setRaw(s: S): void;                         // Sets state bypassing reducer
    on(cb: (state: S) => void): () => void;     // Subscribe on the state change, returns unsubscribe function
    off(cb: (state: S) => void): void;          // Unsubscribe off the state change
}
  • use() is a React Hook that will trigger component re-render when the Shared State changes. It is subject to React Hook usage rules, just like any other Hook. It can only be called from the inside of a functional component.

All other functions are plain js functions. They can be called from anywhere.

  • get() gets current Shared State value. Mutation of the returned value will modify the underlying Shared State value, but won't trigger re-renders or calls of the subscribers.

  • set() updates Shared State value. Accepts either a new value or a function that accepts previous value and returns the new value. The new value should be of type S if no reducer is passed to createShared() or of type A if there is. (Of course, nothing prevents you having S === A which is a very useful case by itself.) The call of the function will trigger re-render of the components that are mounted and use() this Shared State.

  • setRaw() updates Shared State value bypassing the reducer completely. Can accept only the new value itself, not a function that produces the new value. Primarily used for extensions and libraries. Direct usage from normal user code is not recommended.

  • on() and off() allow to manually subscribe and unsubscribe to/from Shared State changes. See Shared State Interaction for usage example.

All SharedState functions are guaranteed to be stable. It’s safe to omit them from the useEffect or other hooks dependency lists.

All SharedState functions do NOT require bind. They are really just functions and NOT class methods.

createShared() function API

// S - State type
// A - Reducer input type (if Reducer is present)
// I - Initializer input type (if Reducer and Initializer are both present)

type Reducer<S, A> = (previousState: S, input: A) => S;
type ReducerAndInit<S, A, I> = [ Reducer<S, A>, (initArg: I) => S ];
type ReducerOrReducerWithInit<S, A> = Reducer<S, A> | ReducerAndInit<S, A, S>;

function createShared<S>(initValue: S, reducer?: ReducerOrReducerWithInit<S, S>): SharedState<S, S>;
function createShared<S, A>(initValue: S, reducer: ReducerOrReducerWithInit<S, A>): SharedState<S, A>;
function createShared<S, A, I>(initValue: I, reducer: ReducerAndInit<S, A, I>): SharedState<S, A>;

createShared() takes two arguments: an initialValue (required) and a reducer (optional).

The reducer is either a Reducer function or a tuple (an array) of two functions, first of which is a Reducer and second is an Initializer.

Usage with class components

Whoosh primarily targets the "new way" of doing thins in React. That is, when the entire application is build using only functional components. But, if you have to support a class component, you can manually subscribe on() the Shared State change in componentWillMount() and unsubscribe off() the Shared State change in componentWillUnmount().

Changelog

1.0.12

  • Add setRaw() method to SharedState

1.0.11

  • Fix rare bug of component no-update when SharedState changes between component first render and effect call

1.0.10

  • Remove freeze for the underlying state object. That was a bad idea...

1.0.9

  • Improve createShared() typing

1.0.8

  • Improve toLocalStorage() typing

1.0.8

  • Update docs
  • Add reducer partialUpdate

1.0.6

  • Improve reducer-compose typing

1.0.5

  • Add reducers arrayOp and setOp to the reducer library and update docs accordingly
  • Underlying state object is now freezed in order to prevent modifications

1.0.4

  • Add initializer function to the reducer argument of createShared()
  • Add reducer library:
    • toLocalStorage() reducer
    • compose() - a function for reducer composition
  • Build with rollup

1.0.1

  • Initial release

License

© github.com/AlexIII

MIT

1.0.12

7 months ago

1.0.11

12 months ago

1.0.10

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago