Skip to content

Latest commit

 

History

History
595 lines (444 loc) · 28.4 KB

README.md

File metadata and controls

595 lines (444 loc) · 28.4 KB

react-stores

react-stores

build status npm bundlephobia minzip npm version with types devtool extension

Shared states for React.

How to install

npm i react-stores --save

Demo

Online demo

For local demo clone this repo and run the script below inside the dir, then go to http://localhost:9000 in your browser

npm i && npm run demo

Tests

npm run test

How to use

Here you are a few examples

Create a Store

// myStore.ts
import { Store } from 'react-stores';

export interface IMyStoreState {
  counter: number;
}

export const myStore = new Store<IMyStoreState>({
  counter: 0, // initial state values
});

React useHooks

import React, { FC } from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';

export const Component: FC = () => {
  const myStoreState = useStore(myStore);

  return (
    <div>
      {myStoreState.counter}
    <div/>
  );
}

// Invoke this code somewhere outside Component and it will be re-rendered
myStore.setState({
  counter: 2,
});

Look here 👉more about use hooks

Event-driven component

// EventDrivenComponent.tsx
import React from 'react';
import { StoreEvent, StoreEventType } from 'react-stores';
import { myStore, IMyStoreState } from './myStore';

interface State {
  myStoreState: IMyStoreState;
}

export class EventDrivenComponent extends React.Component<any, State> {
  private storeEvent: StoreEvent<IMyStoreState> = null;

  state: State = {
    myStoreState: myStore.state,
  };

  comonentDidMount() {
    // Add store state event binder
    this.storeEvent = myStore.on(
      StoreEventType.All,
      (storeState: IMyStoreState, prevState: IMyStoreState, type: StoreEventType) => {
        this.setState({
          myStoreState: storeState,
        });
      },
    );
  }

  componentDidUnmount() {
    // Remove store state event binder
    this.storeEvent.remove();
  }

  render() {
    return <p>Counter: {this.state.myStoreState.counter.toString()}</p>;
  }
}

Component with followStore decorator

// FollowStoreComponent.tsx
import React from 'react';
import { followStore } from 'react-stores';
import { myStore } from './myStore';

// You can use multiple follows
// @followStore(myStore)
// @followStore(myOtherStore)
@followStore(myStore)
export class CounterDecorator extends React.Component {
  public render() {
    return <p>Counter: {myStore.state.counter.toString()}</p>;
  }
}

Advanced Component Hooks

import React from 'react';
import { useStore } from 'react-stores';
import { myStore, IMyStoreState } from './myStore';

interface IMappedState {
  counter: string;
}

interface IProps {
  index: number;
}

function recursiveFibonacci(num: number) {
  if (num <= 1) {
    return 1;
  }
  return recursiveFibonacci(num - 1) + recursiveFibonacci(num - 2);
}

export const MyHookComponent: React.FC<IProps> = (props: IProps) => {
  // Memoize your mapState function
  const mapState = React.useCallback(
    (state: IMyStoreState): IMappedState => ({
      counter: recursiveFibonacci(state.counter), // Very long operation
    }),
    [props.index],
  );

  // Get your state form store
  const { counter } = useStore<IMyStoreState, IMappedState>(myStore, mapState);

  return <p>Counter: {counter}</p>;
};

Mutating store state

import { myStore } from './myStore';

myStore.setState({
  counter: 9999,
});

Read store state value

import { myStore } from './myStore';

console.log(myStore.state.counter); // 9999

useIsolatedStore

import React, { FC } from 'react';
import { useIsolatedStore } from 'react-stores';

interface IMyStoreState {
  counter: number;
}

const initialState = {
  counter: 0,
};

export const Component: FC<{ name: sting }> = ({ name }) => {
  const myStore = useIsolatedStore<IMyStoreState>(initialState, {
    persistence: true,
    immutable: true,
    name,
  });

  const handleIncrement = React.useCallback(() => {
    myStore.setState({
      counter: myStore.state.counter + 1,
    });
  }, [myStore.state.counter]);

  return <button onClick={handleIncrement}>{myStore.state.counter}</button>;
};

API

Store

Store constructor

Argument Type Optional Description
initialState StoreState No Initial state values object
options StoreOptions Yes Setup store as you need with immutable, persistence and etс.
persistenceDriver StorePersistentDriver<StoreState> Yes StorePersistentDriver<StoreState> class instance

Example

StoreState

This can be any interface describes your store's state.

initialState

Any object corresponding to StoreState interface.

StoreOptions

Property Type Default Optional Description
immutable boolean false Yes Object.freeze(...) for store state instances, when disabled you have fully mutable states, but increased performance, for more see
persistence boolean false Yes Enables persistent mode using LocalStorage persistence of custom StorePersistentDriver. If you want use two persistence stores with identical interface you must set name props. In other case both of stores will be use one key in storage.
name string null Yes Uses for name in storage if persistence flag is true. If name not defined name for persistent will be created from initialState interface.
setStateTimeout number 0 Yes Store state updates with timeout

Store methods

Check demo here.

Method name Arguments Returns Description
setState newState: Partial<StoreState> void Set store's state to provided new one can be partial
resetState No void Reset srote to it's initialState
update No void Force update all bound components and emit events
on eventType, callback* +2overload StoreEvent<StoreState> Subscribe to store state event listener
resetPersistence No void Reset persistent state
resetDumpHistory No void Reset all persistent history states
saveDump No number Save state dump and get its ID which is represented in number of current time milliseconds
removeDump timestamp: number void Remove state dump by ID
restoreDump timestamp: number void Replace current state to one from history by ID
getDumpHistory No number[] Get dump history IDs
* Store methods: on() arguments
on() overloads
// Use this to get the whole store state
eventType: StoreEventType | StoreEventType[],
callback: (storeState: StoreState, prevState: StoreState, type: StoreEventType) => void

Use this overload if you need some optimization.

// Use this to get only specific keys
eventType: StoreEventType | StoreEventType[],
includeKeys: Array<keysof StoreState>
callback: (storeState: StoreState, prevState: StoreState, includeKeys: Array<keysof StoreState>, type: StoreEventType) => void

StoreEvent

StoreEvent methods

Method name Arguments Returns Description
remove No void Unsubscribe from store state event listener

StoreEventType Enum

Value The event will be emitted
All After every other event type emits
Init Once, as soon as the event has been bound
Update Each time after store was updated
DumpUpdated Each time after persistent store's dump was updated

Example

followStore

We do not recomend use this way to connected with your stores, because it have no any performance techniques

Argument Type Optional Description
store Store<StoreState> No followed store

Example

useStore

Use useStore only with store argument makes many performance issues. Instead, we recommend using includeKeys or mapState for optimizations and custom compare for perfect optimization.

Argument Type Optional Description
store Store<StoreState> No followed store*
eventType StoreEventType | StoreEventType[] Yes re-render only on specific events
mapState callback Yes The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.
compare callback Yes The optional comparison function also enables using something like Lodash's _.isEqual() or Immutable.js's comparison capabilities. More about compare
* To have more control over re-renders use eventType, mapState and compare their arguments. See more in optimization.
import React from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';

export const CounterComponent = ({ value }) => {
  const store = useStore(myStore);

  // Do not abuse this in a real app with complicated stores.
  // The component will not automatically update if the myStore state changes.
  return <div>{store.anyOfProps}</div>;
};

With includeKeys. example.

Argument Type Optional Description
store Store<StoreState> No followed store
includeKeys Array<keyof StoreState> No followed keys from store
eventType StoreEventType | StoreEventType[] Yes re-render only on specific events

Or with mapState and compare only. Example.

Argument Type Optional Description
store Store<StoreState> No followed store
mapState callback No The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.
compare callback Yes The optional comparison function also enables using something like Lodash' _.isEqual() or Immutable.js's comparison capabilities. More about compare

Or use with second argument called options (legacy).

Argument Type Optional Description
store Store<StoreState> No followed store
options Object No legacy, use an another overload

useStore options

This is legacy, please use another useState overload.

Argument Type Optional Description
eventType StoreEventType | StoreEventType[] Yes re-render only on specific events
mapState callback Yes The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.
compare callback Yes The optional comparison function also enables using something like Lodash's _.isEqual() or Immutable.js's comparison capabilities. More about compare

mapState

This should take the first argument called state, optionally the second argument called prevState, and also takes optionally type as the third argument and returns a plain object containing data that connected component(s) listens to. Selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.

type TMapState<StoreState, MappedState> = (
  storeState: StoreState,
  prevState?: StoreState,
  type?: StoreEventType,
) => MappedState;

compare

When store changes, useState() with mapState calling a compare of the previous mapState result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render. It should return true if compared states are equal. By default, it uses strict === reference equality checks for next and previous mapped state. It works only if you map primitives. Check this compare explanation.

type TCompare<MappedState> = (storeMappedState: MappedState, prevStoreMappedState: MappedState) => boolean;

useIsolatedStore

If you want to use isolated persistent stores dynamically with identical interface you must set name property inside passed options. In other case both of stores will be use one key in storage. Check demo here.

Argument Type Optional Description
initialState StoreState No Initial state values object
options StoreOptions Yes Setup store as you need with immutable, persistence and etс.
persistenceDriver StorePersistentDriver<StoreState> Yes StorePersistentDriver<StoreState> class instance

Persistence

A store instance can be persistent from session to session in case you've provided StorePersistentDriver to it. React-stores includes built-in StorePersistentLocalStorageDriver, it saves store state into the LocalStore* using name or generated hash-name based on initialState interface. Check demo here.

* For React Native you have to provide your own StorePersistentDriver see below.

const myStore = new Store<IMyStoreState>(initialState, new StorePersistentLocalStorageDriver('myStore'));

You can implement your own persistence driver by implementing StorePersistentDriver abstract class.

Optimization

If you need to solve performance problems in the Components connected to stores, react-stores offers you tools to help you fix performance issues. Check demo here.

Use includeKeys in StoreEvent

You can prevent unnecessary update calls with includeKeys. In this case, events calls areSimilar function to compare previous state and next state using only the keys you provided to the includeKeys array.

comonentDidMount() {
  // You call setState each time when storeState changes even if storeState.mapOfObjects does not exist
  // It's not a big deal while you get only primitives from a store state, like strings or numbers
  this.storeEvent = myStore.on(
    StoreEventType.All,
    (storeState) => {
      this.setState({
        momentousMap: storeState.mapOfObjects,
      });
    },
  );
}
comonentDidMount() {
  //You can prevent update call for unnecessary keys in
  // and watch only for an important key for this component
  this.storeEvent = myStore.on(
    StoreEventType.All,
    // Watch only for mapOfObjects key from the store
    ['mapOfObjects'],
    (storeState) => {
      // The callback is fired only when mapOfObjects key was changed
      this.setState({
        momentousMap: storeState.mapOfObjects,
      });
    },
  );
}

Use includeKeys in useStore

You can prevent unnecessary update calls with includeKeys passed to the useStore. In this case, events calls the areSimilar function to compare previous state and next state uses only keys witch you pass into includeKeys.

// You call setState each time when storeState changes even if storeState.mapOfObjects does not exist
// It's not a big deal when you grab a primitives from store, like a strings or a numbers
const { mapOfObjects } = useStore(myStore, StoreEventType.Update /* Optional */);
// You can prevent update call for unnecessary keys in the store
// and watch only for important keys for this component
const { mapOfObjects } = useStore(myStore, ['mapOfObjects'], StoreEventType.Update /* Optional */);

Use mapState in useStore

Let's look at this example. In this case, CounterComponent will re-render every time after any of the keys in myStore were changed. This happens because after changing we get a new copy of the store state and forcing the update.

import React from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';

export const CounterComponent = ({ value }) => {
  // Component re-renders when myStore state changes
  const counter = useStore(myStore);

  /*
   * This code executes when myStore state changes
   */
  return <div>{counter}</div>;
};

You can use mapState function to pick primitives from state and pass them into your component.

import React from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';

export const CounterComponent = ({ value }) => {
  // Okay, now component will re-render only when the counter property actually changed
  // mapState function returns primitive
  const counter = useStore(myStore, state => state.counter);

  return <div>{counter}</div>;
};

It works because useHook uses strict === reference equality that checks next mapped state and previous mapped state. For primitives, it works perfectly, but let's look at this:

import React from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';

export const CounterComponent = ({ value }) => {
  // Now component re-renders every time we change store state
  // It is also relevant when you map two or more keys
  const { counter } = useStore(myStore, state => ({ counter: state.counter }));

  /*
   * All this code will be executed when myStore were changed
   */

  return <div>{counter}</div>;
};

It happens because mapState(nextState) does not equal the previous mapState, they are different objects. You can fix it with compare function.

Use compare in useStore

When you use mapState with only few keys, your component will re-render even if keys did not changed. Take a look:

import React from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';

export const CounterComponent = ({ value }) => {
  // Now component re-renders every time
  // It is also relevant when you map two or more keys
  const { counter } = useStore(myStore, state => ({ counter: state.counter }));

  /*
   * All this code will be executed when myStore were changed
   */

  return <div>{counter}</div>;
};

It happens because mapState(nextState) not equal previous mapState, they are different objects. Let's use compare function to fix it.

import React from 'react';
import { useStore } from 'react-stores';
// You can use `areSimilar` function exported from react-stores
import { areSimilar } from 'react-stores';
import { myStore } from './myStore';

export const CounterComponent = ({ value }) => {
  // Re-render component only when counter or anotherValue was changed
  const { counter, anotherValue } = useStore(
    myStore,
    state => ({ counter: state.counter, anotherValue: state.anotherValue }),
    // Use your custom compare function to prevent re-renders
    // if the store was changed but mapped values are the same.
    (a, b) => a.counter === b.counter && someCompareFunction(a.anotherValue, b.anotherValue),
  );

  return (
    <div>
      {counter}, {anotherValue.id}
    </div>
  );
};

Debugging

Install extension for chrome devtools for better debugging.