Skip to content
This repository has been archived by the owner on May 20, 2022. It is now read-only.

Commit

Permalink
Merge 52e2019 into 3fea3d3
Browse files Browse the repository at this point in the history
  • Loading branch information
jhonnymichel committed Feb 9, 2020
2 parents 3fea3d3 + 52e2019 commit 19e873c
Show file tree
Hide file tree
Showing 4 changed files with 395 additions and 72 deletions.
69 changes: 38 additions & 31 deletions README.md
@@ -1,8 +1,7 @@
# React Hook Store
[![npm version](https://badge.fury.io/js/react-hookstore.svg)](https://badge.fury.io/js/react-hookstore) [![Build Status](https://travis-ci.org/jhonnymichel/react-hookstore.svg?branch=master)](https://travis-ci.org/jhonnymichel/react-hookstore) [![Coverage Status](https://coveralls.io/repos/github/jhonnymichel/react-hookstore/badge.svg?branch=master)](https://coveralls.io/github/jhonnymichel/react-hookstore?branch=master)

A very simple and small (1k gzipped!) state management lib for React that uses the bleeding edge React's `useState` hook.
Which basically means no magic behind the curtains, only pure react APIs being used to share state across components.
A very simple and small (less than 2k gzipped!) state management lib for React using hooks.

Try it on [Codesandbox!](https://codesandbox.io/s/r58pqonkop)

Expand All @@ -11,7 +10,7 @@ Try it on [Codesandbox!](https://codesandbox.io/s/r58pqonkop)
- Usage
- [Basic](#usage_basic)
- [Referencing stores](#usage_namespace)
- [Reducer powered stores](#usage_reducer)
- [Using reducers to update state](#usage_reducer)
- [More examples](https://codesandbox.io/s/r58pqonkop)
- API
- [createStore](#api_createStore)
Expand All @@ -30,9 +29,7 @@ You can install the lib through NPM or grab the files in the `dist` folder of th
## <a name="usage">Usage</a>
### <a name="usage_basic">Basic</a>

This is the most basic implementation of the library. create a store with its initial state.
Later, call `useStore` inside components to retrieve its state and setState method.
The value passed as the first argument to the setState method will be the new state. no reducer required (but you can use a reducer, see the advanced example down below).
This is the most basic implementation of the library:

```javascript
import React from 'react';
Expand All @@ -41,7 +38,6 @@ import { createStore, useStore } from 'react-hookstore';
createStore('clickStore', 0);

function StatefullHello() {
// just use the useStore method to grab the state and the setState methods
const [ timesClicked, setClicks ] = useStore('clickStore');

return (
Expand All @@ -54,7 +50,6 @@ function StatefullHello() {
}

function AnotherComponent() {
// you can name the state whatever you want
const [ timesClicked ] = useStore('clickStore');
return (
<div>
Expand All @@ -64,9 +59,13 @@ function AnotherComponent() {
)
}
```
Steps to reproduce:

- Create a store with its initial state.
- Later, call `useStore` inside components to retrieve its state and setState method, that we called timesClicked and setClicks.
- The value passed as the first argument to the setClicks method will be the new state.

### <a name="usage_namespace">Referencing stores</a>
It is possible to create multiple stores in an app.
Stores can be referenced by using their instance that is returned by the createStore method, as well as using their name.

```javascript
Expand Down Expand Up @@ -95,10 +94,10 @@ function StatefullHello() {
);
}
```
Both methods can be used and mixed according to the needs, but we recomend using the instance identifiers.
Both methods can be used and mixed according to the needs, but it is recomended to use the instance identifiers.

### <a name="usage_reducer">Reducer powered stores</a>
We can delegate the state management to reducers (just like redux!) if we want.
### <a name="usage_reducer">Using reducers to update state</a>
We can delegate the state management to reducers (just like redux!) if we want:
```javascript
import React from 'react';
import { createStore, useStore } from 'react-hookstore';
Expand Down Expand Up @@ -168,48 +167,56 @@ function TodoList() {

export { TodoList, AddTodo };
```

Steps to reproduce:

- Create a store with an aditional third parameter: a reducer function.
- Later, call `useStore` inside components to retrieve its state and dispatch method.
- call dispatch and provide data as the first argument. Although data can be anything, we are using the pattern of `{ type, payload }`, made popular by redux.


### More examples
Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop)

## Methods API
### <a name="api_createStore">`createStore(name:String, state?:*, reducer?:Function):StoreInterface`</a>
### <a name="api_createStore">`createStore(name, state?, reducer?) -> StoreInterface`</a>
Creates a store to be used across the entire application. Returns a StoreInterface object.
### Arguments
#### `name:String`
#### `name: String`
The namespace for your store, it can be used to identify the store across the application.
#### `state:* = {}`
#### `state: any = {}`
The store's initial state. it can be any data type. defaults to an empty object. Optional
#### `reducer:Function`
You can specify a reducer function to take care of state changes. the reducer functions receives two arguments, the previous state and the action that triggered the state update. the function must return a new state, if not, the new state will be `null`. Optional
#### `reducer: (state:any, data:any) -> any`
You can specify a reducer function to take care of state changes. the reducer functions receives two arguments, the previous state and the data dispatched. the function must return a new state, if not, the new state will be `null`. Optional

### <a name="api_getStoreByName">`getStoreByName(name:String):StoreInterface`</a>
### <a name="api_getStoreByName">`getStoreByName(name) -> StoreInterface`</a>
Finds a store by its name and returns its instance.
### Arguments
#### `name:String`
#### `name: String`
The name of the store.

## Objects API
### <a name="api_storeInterface">`StoreInterface`</a>
The store instance that is returned by the createStore and getStoreByName methods.
### Interface
#### `name:String`
#### `name: String`
The name of the store;
#### `getState:Function():*`
#### `getState: () -> state`
A method that returns the store's current state
#### `setState:Function(state:*, callback?:Function)`
#### `setState: (state: any, callback?: (state:any) -> void)`
Sets the state of the store. works if the store does not use a reducer state handler. Otherwise, use `dispatch`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument.
#### `dispatch:Function(action:*, callback?:Function)`
Dispatches whatever is passed into this function to the store. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument.
#### `subscribe:Function(callback:Function):unsubscribe:Function`
The callback function will be invoked everytime the store state changes. If the store is reducer-based, the callback function will be called with `action` as the first argument and `state` as the second. otherwise, it'll be called with `state` as the only argument.
#### `dispatch(data: any, callback?: (state: any) -> void)`
Dispatches data to update the state. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument.
#### `subscribe(callback: (state: any, data?: any) -> void) -> unsubscribe: () -> void`
The callback function will be invoked everytime the store state changes. If the store is reducer-based, the callback function will be called with the state and the dispatched data as arguments. otherwise, it'll be called with state as the only argument.

the subscribe method returns a function that can be called in order to cancel the subscription for the callback function.

## React API
### <a name="api_useStore">`useStore(identifier:String|StoreInterface):Array[state, setState|dispatch]`</a>
### <a name="api_useStore">`useStore(identifier) -> [state, setState|dispatch]`</a>
A function that returns a pair with the current state and a function to trigger state updates for the specified store.
### Arguments
#### Identifier:String|StoreInterface
#### Identifier: String|StoreInterface
The store identifier. It can be either its string name or its StoreInterface instance returned by a createStore or getStoreByName method.

# <a name="migration">Migrating from v1.0 to v1.1</a>
Expand All @@ -221,11 +228,11 @@ createStore({state: 0});
createStore({
name: 'store',
state: 0,
reducer(state, action) {
return state + action;
reducer(state, data) {
return state + data;
}
})
// v1.1
createStore('myStore', 0);
createStore('store', 0, (state, value) => state + action);
createStore('store', 0, (state, data) => state + data);
```
98 changes: 69 additions & 29 deletions src/index.js
Expand Up @@ -4,6 +4,7 @@ let stores = {};
let subscriptions = {};

const defaultReducer = (state, payload) => payload;
const defaultMemoFn = (state) => state;

/** The public interface of a store */
class StoreInterface {
Expand All @@ -17,14 +18,9 @@ class StoreInterface {

/**
* Subscribe to store changes
* @callback callback - The function to be invoked everytime the store is updated
* @param {(state:any, data:any) => void} callback - The function to be invoked everytime the store is updated
* @return {Function} - Call the function returned by the method to cancel the subscription
*/

/**
*
* @param {callback} state, action
*/
subscribe(callback) {
if (!callback || typeof callback !== 'function') {
throw `store.subscribe callback argument must be a function. got '${typeof callback}' instead.`;
Expand All @@ -39,11 +35,18 @@ class StoreInterface {
}
}

setState() {
/**
* Set the store state
* @param {any} data - The new state value.
*/
setState(data) {
console.warn(`[React Hookstore] Store ${this.name} uses a reducer to handle its state updates. use dispatch instead of setState`)
}

dispatch() {
/**
* Dispatch data to the store reducer
* @param {any} data - The data payload the reducer receives
*/
dispatch(data) {
console.warn(`[React Hookstore] Store ${this.name} does not use a reducer to handle state updates. use setState instead of dispatch`)
}
}
Expand All @@ -60,49 +63,70 @@ function getStoreByIdentifier(identifier) {
* Creates a new store
* @param {String} name - The store namespace.
* @param {*} state [{}] - The store initial state. It can be of any type.
* @callback reducer [null]
* @param {(state:any, data:any) => any} reducer [null] - The reducer handler. Optional
* @returns {StoreInterface} The store instance.
*/

/**
*
* @param {reducer} prevState, action - The reducer handler. Optional.
*/
export function createStore(name, state = {}, reducer=defaultReducer) {
if (typeof name !== 'string') {
throw 'Store name must be a string';
}
if (stores[name]) {
throw `Store with name ${name} already exists`;
}


const store = {
state,
reducer,
setState(action, callback) {
this.state = this.reducer(this.state, action);
this.setters.forEach(setter => setter(this.state));
setState(data, callback) {
const isPrimitiveStateWithoutReducerAndIsPreviousState =
this.reducer === defaultReducer
&& data === this.state
&& typeof data !== 'object';

if (isPrimitiveStateWithoutReducerAndIsPreviousState) {
if (typeof callback === 'function') callback(this.state)
return;
}

const currentState = this.state;
const newState = this.reducer(this.state, data);
this.state = newState;

this.updatersPerMemoFunction.forEach((updaters, memoFn) => {
const prevResult = memoFn(currentState);
const newResult = memoFn(newState);
if (prevResult === newResult) {
return;
}
for (let updateComponent of updaters) {
updateComponent(this.state);
}
});

if (subscriptions[name].length) {
subscriptions[name].forEach(c => c(this.state, action));
subscriptions[name].forEach(c => c(this.state, data));
}

if (typeof callback === 'function') callback(this.state)
},
setters: []
updatersPerMemoFunction: new Map(),
};

store.setState = store.setState.bind(store);
store.updatersPerMemoFunction.set(defaultMemoFn, new Set())
stores = Object.assign({}, stores, { [name]: store });
subscriptions[name] = [];
store.public = new StoreInterface(name, store, reducer !== defaultReducer);

stores = Object.assign({}, stores, { [name]: store });
store.public = new StoreInterface(name, store, reducer !== defaultReducer);
return store.public;
}

/**
* Returns a store instance based on its name
* @callback {String} name - The name of the wanted store
* @name {String} name - The name of the wanted store
* @returns {StoreInterface} the store instance
*/

export function getStoreByName(name) {
try {
return stores[name].public;
Expand All @@ -114,22 +138,38 @@ export function getStoreByName(name) {
/**
* Returns a [ state, setState ] pair for the selected store. Can only be called within React Components
* @param {String|StoreInterface} identifier - The identifier for the wanted store
* @param {(state:any) => any} memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes.
* @returns {Array} the [state, setState] pair.
*/
export function useStore(identifier) {
export function useStore(identifier, memoFn=defaultMemoFn) {
const store = getStoreByIdentifier(identifier);
if (!store) {
throw 'store does not exist';
}
if (typeof memoFn !== 'function') {
throw 'memoFn must be a function';
}

const [ state, set ] = useState(store.state);

useEffect(() => {
if (!store.setters.includes(set)) {
store.setters.push(set);
if (!store.updatersPerMemoFunction.has(memoFn)) {
store.updatersPerMemoFunction.set(memoFn, new Set());
}

const updatersPerMemoFunction = store.updatersPerMemoFunction.get(memoFn);

if (!updatersPerMemoFunction.has(set)) {
updatersPerMemoFunction.add(set);
}

return () => {
store.setters = store.setters.filter(setter => setter !== set)
updatersPerMemoFunction.delete(set);
if (!updatersPerMemoFunction.size) {
store.updatersPerMemoFunction.delete(memoFn);
}
}
}, [])

return [ state, store.setState ];
return [ state, store.setState];
}

0 comments on commit 19e873c

Please sign in to comment.