Skip to content

InfiniteXyy/zoov

Repository files navigation

ZOOV

✨ ZOOV = Zustand + module

Build Status npm-v npm-d minzip

Read the docs

Features

  • 😌 Easy: Comfortable type inference
  • ✨ Magic: Update state by just mutate it (with support of immer)
  • 🍳 Tiny: < 200 line code based on Zustand
  • 🧮 Powerful: Modular state management (Redux-like)
  • 📖 Smart: Scope supported with Algebraic Effects
  • 📦 Flexible: Attach state/actions inside or outside React

Quick Start

You can try it on StackBlitz or CodeSandbox

Or install locally

yarn add immer zustand # peer dependencies
yarn add zoov

First Glance

const { use: useCounter } = defineModule({ count: 0 })
  .actions({
    add: (draft) => draft.count++,
    minus: (draft) => draft.count--,
  })
  .computed({
    doubled: (state) => state.count * 2,
  })
  .build();

const App = () => {
  const [{ count }, { add }] = useCounter();
  return <button onClick={add}>{count}</button>;
};

// state is shared
const App2 = () => {
  const [, , { doubled }] = useCounter();
  return <div>doubled: {doubled}</div>;
};

More Examples

Use Methods

import { effect } from 'zoov/effect';

const counterModule = defineModule({ count: 0 })
  .actions({
    add: (draft) => draft.count++,
    minus: (draft) => draft.count--,
  })
  .methods(({ getActions }) => {
    return {
      addAndMinus: () => {
        getActions().add();
        getActions().add();
        setTimeout(() => getActions().minus(), 100);
      },
      // async function is supported
      asyncAdd: async () => {
        await something();
        getActions().add();
      },
      // [TIPS] If you want to `rxjs` in `zoov`, your should first install `rxjs`
      addAfter: effect<number>((payload$) =>
        payload$.pipe(
          exhaustMap((timeout) => {
            return timer(timeout).pipe(tap(() => getActions().add()));
          }),
        ),
      ),
    };
  })
  // using `this` is allowed now! remember to set `noImplicitThis` true in tsconfig
  .methods({
    addTwo() {
      this.getActions().add();
      this.getActions().add();
    },
  })
  .build();

Use Selector

const { use: useCounter } = defineModule({ count: 0, input: 'hello' })
  .actions({
    add: (draft) => draft.count++,
    setInput: (draft, value: string) => (draft.input = value),
  })
  .build();

const App = () => {
  // <App /> will not rerender unless "count" changes
  const [count] = useCounter((state) => state.count);
  return <span>{count}</span>;
};

Additionally, you can install react-tracked and use useTrackedModule to automatically generate selector

// will not rerender unless "count" changes
const [{ count }, { add }] = useTrackedModule(module);

Use subscriptions

const module = defineModule({ pokemonIndex: 0, input: '' })
  .subscribe((state, prevState) => console.log(state)) // subscribe to the whole store
  .subscribe({
    selector: (state) => state.pokemonIndex, // only subscribe to some property
    listener: async (pokemonIndex, prev, { addCleanup }) => {
      const abortController = new AbortController();
      const abortSignal = abortController.signal;
      addCleanup(() => abortController.abort());
      const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonIndex}`, { signal: abortSignal });
      console.log(await response.json());
    },
  })
  .build();

Use Middleware

// see more examples in https://github.com/pmndrs/zustand/blob/master/src/middleware.ts
const module = defineModule({ count: 0 })
  .actions({ add: (draft) => draft.count++ })
  .middleware((store) => persist(store, { name: 'counter' }))
  .build();

Use internal Actions

// a lite copy of solid-js/store, with strict type check
const { useActions } = defineModule({ count: 0, nested: { checked: boolean } }).build();

const { $setState, $reset } = useActions();

$setState('count', 1);
$setState('nested', 'checked', (v) => !v);
$reset();

Use Provider

import { defineProvider } from 'zoov';

const CustomProvider = defineProvider((handle) => {
  // create a new module scope for all its children(can be nested)
  handle(yourModule, {
    defaultState: {},
  });
  handle(anotherModule, {
    defaultState: {},
  });
});

const App = () => {
  // if a module is not handled by any of its parent, then used global scope
  return (
    <div>
      <CustomProvider>
        <Component />
      </CustomProvider>
      <Component />
    </div>
  );
};

Attach state outside components

// by default, it will get the state under global scope
const actions = module.getActions();
const state = module.getState();

// you can specify the scope with params
const context = useScopeContext();
const scopeActions = module.getActions(context);