Skip to content

Latest commit

 

History

History
340 lines (273 loc) · 8.59 KB

README.md

File metadata and controls

340 lines (273 loc) · 8.59 KB

use-one: A simple state management lib for React.js.

Test and Release npm version license

Intro

use-one.js is a simple state management lib for React.js.

Features

  • No more complex concepts, only hooks
  • Easy share state anywhere
  • Easy persist store or your hooks state
  • Tiny size (gzip ~2KB)
  • Write in TypeScript, Typesafe

Table of Contents

Install

npm

npm install use-one  --save

pnpm

pnpm install use-one

Usage

Simple Demo

// stores/count.ts
import { create, EventBus, eventBus } from 'use-one';
// import { type StrictPropertyCheck } from 'use-one';

const initialState = { count: 0 };
const [use, store] = create(initialState);

const actions = {
  use,
  get state() {
    return store.getState();
  },
  increment() {
    store.setState({ count: this.state.count + 1 });
  },
  decrement() {
    store.setState({ count: this.state.count - 1 });
  },
  // setState: 1 // Be careful!, If you uncomment this line, 
                 // the property will replace by the below code, how to avoid this, 
                 // check TypeScript Advanced Demo part in the document.
};

export const countStore = Object.assign(actions, store);

Use the hook

// CountExample.tsx
import * as React from 'react';
import { countStore } from './stores/count';

const Counter = () => {
  const [state] = countStore.use();
  const { count } = state;
  // const { count } = countStore.state;

  return (
    <div>
      <button onClick={countStore.increment}>+1</button>
      <span>{count}</span>
      <button onClick={countStore.decrement}>-1</button>
      <button
        onClick={() => {
          setTimeout(() => {
            countStore.setState({
              count: countStore.state.count + 2,
            });
          }, 2000);
        }}
      >
        async +2
      </button>
    </div>
  );
};

const ShowCountInOtherPlace = () => {
  const [state] = countStore.use();
  return <span>Count: {state.count}</span>;
};

export default function App() {
  return (
    <React.Fragment>
      <ShowCount />
      <Counter />
    </React.Fragment>
  );
}

Using immer

We can wrap a new function that call produceState with immer's produce, for example:

export function produceState(cb: (state: typeof initialState) => void) {
  countStore.setState(produce(cb));
}

Full code:

// stores/count.ts
import { create } from 'use-one';
import { produce } from 'immer';

const initialState = { count: 0 };
const [use, store] = create(initialState);

const computed = {
  get state() {
    return store.getState();
  },
};

const actions = {
  use,
  produce(cb: (state: typeof initialState) => void) {
    store.setState(produce(cb));
  },
  increment() {
    this.produce((state) => {
      state.count++;
    });
  },
  decrement() {
    this.produce((state) => {
      state.count--;
    });
  },
};

export const countStore = Object.assign(actions, computed, store);

Persist store

If you are using React-Native or Expo, Need install @react-native-async-storage/async-storage

import { create, persistStore, wrapState, isClient } from 'use-one';

const initialState = wrapState({ count: 0 }); // -> { ready: false, count: 0 }
const [use, store] = create(initialState);

console.log('isClient', isClient);
isClient &&
  persistStore<typeof initialState>(store, {
    key: '@CACHE_KEY',
    debounce: 100, // optional, default 100ms
    transform: (state) => state, // optional, transform the state before to `setState`
  });

const actions = {
  use,
  get state() {
    return store.getState();
  },
  increment() {
    store.setState({ count: this.state.count + 1 });
  },
  decrement() {
    store.setState({ count: this.state.count - 1 });
  },
};
export const countStore = Object.assign(actions, store);

Persist store in SSR application

To prevent hydration error in SSR application(like Next.js, Remix..etc.), we can do this:

    1. Use onPersistReady to subscribe ready event to persist:
import {
  create,
  persistStore,
  wrapState,
  isClient,
  onPersistReady,
} from 'use-one';

const initialState = wrapState({ count: 0 }); // -> { ready: false, count: 0 }
const [use, store] = create(initialState);

onPersistReady(() => {
  persistStore<typeof initialState>(store, {
    key: '@CACHE_KEY',
    debounce: 100, // optional, default 100ms
    transform: (state) => state, // optional, transform the state before to `setState`
  });
});

const actions = {
  use,
  get state() {
    return store.getState();
  },
  increment() {
    store.setState({ count: this.state.count + 1 });
  },
  decrement() {
    store.setState({ count: this.state.count - 1 });
  },
};
export const countStore = Object.assign(actions, store);
    1. Add PersistProvider to your components to emit ready event:
import { Provider as PersistProvider } from 'use-one';

export default function Layout({ children }: { children: React.ReactNode }) {
  return <PersistProvider>{children}</PersistProvider>;
}

Persist any hooks state

This is a helper function, no relation with store.

You persist any hooks state. For example, let's persist useState:

If you are using React-Native or Expo, Need install @react-native-async-storage/async-storage

import { useState } from 'react';
import { usePersist } from 'use-one';

export function Counter() {
  const [count, setCount] = useState(0);
  const [isReady, clean] = usePersist<typeof count>({
    key: '@count-store-key',
    getState: () => count,
    setState: setCount,
    // setState: (state) => setCount(state),
  });
  if (!isReady) return <div>Loading</div>;

  return (
    <div>
      <h1>{count}</h1>
      <br />
      <button onClick={() => setCount(count + 1)}>+1</button>
      <br />
      <button onClick={() => setCount(count - 1)}>-1</button>
      <br />
      <button onClick={clean}>Clean Cache</button>
    </div>
  );
}

Advanced TypeScript Demo

The previous simple demo has a problem, if we put setState or getState property to actions object, We do Object.assign(actions, store), it will replace by store's .setState or .getState (Check the API part), so to avoid this kind potensial problem, let's use StrictPropertyCheck:

import { create, type StrictPropertyCheck } from 'use-one';

import { create, EventBus, eventBus } from 'use-one';

const initialState = { count: 0 };
const [use, store] = create(initialState);

const _actions = {
  use,
  get state() {
    return store.getState();
  },
  increment() {
    store.setState({ count: this.state.count + 1 });
  },
  decrement() {
    store.setState({ count: this.state.count - 1 });
  },
  // setState: 1 // If you uncomment this line, the code below will throw an error.
};
const actions: StrictPropertyCheck<typeof _actions> = _actions;

export const countStore = Object.assign(actions, store);

API

  • create - e.g: create<Type>(initialState, Options?: {useEffect?: boolean, name?: string}) if the options useEffect is false, will use useLayoutEffect
    • returns [useHook, store]
      • store methods:
        • .getState() get the state
        • .setState(newState) set the state
        • .forceUpdate() force update
        • .subscribe(cb: (state) => {}) subscribe .setState update, return unsubscribe function
        • .syncState(newState) sync state without update
        • .destroy clear event

Boilerplate Code Generator

Check use-one-templates, it's very useful to create many share states in large application.