Skip to content

A simple state management library in react app. State sharing and persist never been so easy :)

License

Notifications You must be signed in to change notification settings

suhaotian/use-one

Repository files navigation

Test and Release npm version install size license author

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';

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 });
  },
};

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>
  );
}

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.