https://github.com/ibitcy/react-stores
Shared states for React and React Native
https://github.com/ibitcy/react-stores
flux persistence react react-stores reactive states store storestate
Last synced: 6 months ago
JSON representation
Shared states for React and React Native
- Host: GitHub
- URL: https://github.com/ibitcy/react-stores
- Owner: ibitcy
- License: mit
- Created: 2017-02-07T10:29:44.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2023-09-04T10:47:40.000Z (about 2 years ago)
- Last Synced: 2025-04-11T14:58:31.494Z (6 months ago)
- Topics: flux, persistence, react, react-stores, reactive, states, store, storestate
- Language: TypeScript
- Homepage:
- Size: 3.89 MB
- Stars: 38
- Watchers: 4
- Forks: 10
- Open Issues: 14
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# react-stores

[](https://travis-ci.org/ibitcy/react-stores)
[
](https://www.npmjs.com/package/react-stores)

[](https://github.com/ibitcy/react-stores-devtools-extension)Shared states for React.
## How to install
```shell
npm i react-stores --save
```## Demo
[Online demo](https://ibitcy.github.io/react-stores/)
For local demo clone this repo and run the script below inside the dir, then go to [http://localhost:9000](http://localhost:9000) in your browser
```bash
npm i && npm run demo
```## Tests
```bash
npm run test
```## How to use
Here you are a few examples
### Create a Store
```typescript
// myStore.ts
import { Store } from 'react-stores';export interface IMyStoreState {
counter: number;
}export const myStore = new Store({
counter: 0, // initial state values
});
```### React useHooks
```typescript jsx
import React, { FC } from 'react';
import { useStore } from 'react-stores';
import { myStore } from './myStore';export const Component: FC = () => {
const myStoreState = useStore(myStore);return (
{myStoreState.counter}
);
}// Invoke this code somewhere outside Component and it will be re-rendered
myStore.setState({
counter: 2,
});
```Look here 👉[more about use hooks](#use-includekeys-in-usestore)
### Event-driven component
```typescript jsx
// 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 {
private storeEvent: StoreEvent = 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() {
returnCounter: {this.state.myStoreState.counter.toString()}
;
}
}
```### Component with followStore decorator
```typescript jsx
// 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() {
returnCounter: {myStore.state.counter.toString()}
;
}
}
```### Advanced Component Hooks
```typescript jsx
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 = (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(myStore, mapState);return
Counter: {counter}
;
};
```### Mutating store state
```typescript
import { myStore } from './myStore';myStore.setState({
counter: 9999,
});
```### Read store state value
```typescript
import { myStore } from './myStore';console.log(myStore.state.counter); // 9999
```### useIsolatedStore
```typescript tsx
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(initialState, {
persistence: true,
immutable: true,
name,
});const handleIncrement = React.useCallback(() => {
myStore.setState({
counter: myStore.state.counter + 1,
});
}, [myStore.state.counter]);return {myStore.state.counter};
};
```# API
### Store
#### Store constructor
| Argument | Type | Optional | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `initialState` | [`StoreState`](#storestate) | No | Initial state values object |
| `options` | [`StoreOptions`](#storeoptions) | Yes | Setup store as you need with immutable, persistence and etс. |
| `persistenceDriver` | [`StorePersistentDriver`](https://github.com/ibitcy/react-stores/blob/master/src/StorePersistentDriver.ts) | Yes | [`StorePersistentDriver`](https://github.com/ibitcy/react-stores/blob/master/src/StorePersistentDriver.ts) class instance |[Example](#create-a-store)
#### 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](https://ibitcy.github.io/react-stores/?#Performance) |
| `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](https://ibitcy.github.io/react-stores/?#Snapshots).
| Method name | Arguments | Returns | Description |
| ------------------ | ------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ |
| `setState` | `newState: Partial` | `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](#on-overloads) | [`StoreEvent`](#storeevent) | 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
```typescript
// 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-includekeys-in-storeevent).
```typescript
// Use this to get only specific keys
eventType: StoreEventType | StoreEventType[],
includeKeys: Array
callback: (storeState: StoreState, prevState: StoreState, includeKeys: Array, 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](#event-driven-component)
### 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`](#store) | No | followed store |[Example](#component-with-followstore-decorator)
### useStore
Use useStore only with `store` argument makes many performance issues.
Instead, we recommend using [includeKeys](#use-includekeys-in-usestore) or [mapState](#use-mapstate-in-usestore) for optimizations and custom [compare](#use-compare-in-usestore) for perfect optimization.| Argument | Type | Optional | Description |
| ----------- | ---------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `store` | [`Store`](#store) | No | followed store\* |
| `eventType` | [`StoreEventType`](#storeeventtype-enum) `\|` [`StoreEventType[]`](#storeeventtype-enum) | Yes | re-render only on specific events |
| `mapState` | [`callback`](#mapState) | Yes | The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time. |
| `compare` | [`callback`](#compare) | Yes | The optional comparison function also enables using something like Lodash's \_.isEqual() or Immutable.js's comparison capabilities. More about [compare](#compare) |##### \* To have more control over re-renders use eventType, mapState and compare their arguments. See more in [optimization](#optimization).
```typescript jsx
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{store.anyOfProps};
};
```With `includeKeys`. [example](#use-includekeys-in-usestore).
| Argument | Type | Optional | Description |
| ------------- | ---------------------------------------------------------------------------------------- | -------- | --------------------------------- |
| `store` | [`Store`](#store) | No | followed store |
| `includeKeys` | `Array` | No | followed keys from store |
| `eventType` | [`StoreEventType`](#storeeventtype-enum) `\|` [`StoreEventType[]`](#storeeventtype-enum) | Yes | re-render only on specific events |Or with [`mapState`](#mapState) and [`compare`](#compare) only. [Example](#use-mapstate-in-usestore).
| Argument | Type | Optional | Description |
| ---------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `store` | `Store` | No | followed store |
| `mapState` | [`callback`](#mapState) | No | The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time. |
| `compare` | [`callback`](#compare) | Yes | The optional comparison function also enables using something like Lodash' `_.isEqual()` or Immutable.js's comparison capabilities. More about [compare](#compare) |Or use with second argument called options (legacy).
| Argument | Type | Optional | Description |
| --------- | ----------------------------- | -------- | -------------------------------------------- |
| `store` | `Store` | No | followed store |
| `options` | [`Object`](#usestore-options) | No | legacy, use an another [overload](#usestore) |#### useStore options
This is legacy, please use another useState overload.
| Argument | Type | Optional | Description |
| ----------- | ---------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `eventType` | [`StoreEventType`](#storeeventtype-enum) `\|` [`StoreEventType[]`](#storeeventtype-enum) | Yes | re-render only on specific events |
| `mapState` | [`callback`](#mapState) | Yes | The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time. |
| `compare` | [`callback`](#compare) | Yes | The optional comparison function also enables using something like Lodash's \_.isEqual() or Immutable.js's comparison capabilities. More about [compare](#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.
```typescript
type TMapState = (
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](#use-compare-in-usestore) explanation.
```typescript
type TCompare = (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](https://ibitcy.github.io/react-stores/?#Isolated).
| Argument | Type | Optional | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `initialState` | [`StoreState`](#storestate) | No | Initial state values object |
| `options` | [`StoreOptions`](#storeoptions) | Yes | Setup store as you need with immutable, persistence and etс. |
| `persistenceDriver` | [`StorePersistentDriver`](https://github.com/ibitcy/react-stores/blob/master/src/StorePersistentDriver.ts) | Yes | [`StorePersistentDriver`](https://github.com/ibitcy/react-stores/blob/master/src/StorePersistentDriver.ts) class instance |# Persistence
A store instance can be persistent from session to session in case you've provided [`StorePersistentDriver`](https://github.com/ibitcy/react-stores/blob/master/src/StorePersistentDriver.ts) 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](https://ibitcy.github.io/react-stores/?#Persistent).
\* For React Native you have to provide your own `StorePersistentDriver` see below.
```typescript
const myStore = new Store(initialState, new StorePersistentLocalStorageDriver('myStore'));
```You can implement your own persistence driver by implementing [`StorePersistentDriver`](https://github.com/ibitcy/react-stores/blob/master/src/StorePersistentDriver.ts) 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](https://ibitcy.github.io/react-stores/?#optimization).
### Use includeKeys in StoreEvent
You can prevent unnecessary update calls with `includeKeys`.
In this case, events calls [areSimilar function](https://github.com/ibitcy/react-stores/blob/master/src/compare.ts) to compare previous state and next state using only the keys you provided to the `includeKeys` array.```typescript
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,
});
},
);
}
``````typescript
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](https://github.com/ibitcy/react-stores/blob/master/src/compare.ts) to compare previous state and next state uses only keys witch you pass into `includeKeys`.```typescript
// 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 */);
``````typescript
// 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.```typescript jsx
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{counter};
};
```You can use `mapState` function to pick primitives from state and pass them into your component.
```typescript jsx
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
{counter};
};
```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:
```typescript jsx
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
{counter};
};
```It happens because mapState(nextState) does not equal the previous mapState, they are different objects. You can fix it with [`compare`](#use-compare-in-usestore) 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:
```typescript jsx
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
{counter};
};
```It happens because mapState(nextState) not equal previous mapState, they are different objects. Let's use `compare` function to fix it.
```typescript jsx
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 (
{counter}, {anotherValue.id}
);
};
```# Debugging
Install [extension for chrome](https://github.com/ibitcy/react-stores-devtools-extension#react-stores-devtools-extension) devtools for better debugging.