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

Commit

Permalink
Merge 496055f into 807f899
Browse files Browse the repository at this point in the history
  • Loading branch information
jhonnymichel committed Aug 30, 2019
2 parents 807f899 + 496055f commit cfb7225
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 176 deletions.
40 changes: 18 additions & 22 deletions README.md
Expand Up @@ -10,16 +10,17 @@ Try it on [Codesandbox!](https://codesandbox.io/s/r58pqonkop)
- [Installation](#installation)
- Usage
- [Basic](#usage_basic)
- [Namespacing and referencing stores](#usage_namespace)
- [Referencing stores](#usage_namespace)
- [Reducer powered stores](#usage_reducer)
- API
- [createStore](#api_createStore)
- [getStoreByName](#api_getStoreByName)
- [StoreInterface](#api_storeInterface)
- [useStore](#api_useStore)
- [useStore](#api_useStore)
- [Migrating from v1.0 to v1.1](#migration)

> ⚠️ BREAKING CHANGES: Version 1.1 is not compatible with previous versions. It is easy to update your previous versions' code to work with it, though. [Click here](#migration) to know how.
> ⚠️ BREAKING CHANGES: in version 1.4+, `store.subscribe` was simplified. check the [Store Interface API](#api_storeInterface)
## <a name="installation">Installation</a>
You can install the lib through NPM or grab the files in the `dist` folder of this repository.
Expand Down Expand Up @@ -64,7 +65,7 @@ function AnotherComponent() {
}
```

### <a name="usage_namespace">Namespacing and referencing stores</a>
### <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.

Expand Down Expand Up @@ -109,7 +110,7 @@ const todoListStore = createStore(
todos: [{ id: 0, text: 'buy milk' }]
},
(state, action) => {
// when a reducer is being used, you must return a new state object
// when a reducer is being used, you must return a new state value
switch (action.type) {
case 'add':
const id = ++state.idCount;
Expand All @@ -123,34 +124,25 @@ const todoListStore = createStore(
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return {
...state,
todos: [...state.todos]
};
return state;
}
}
);

function AddTodo() {
const [state, dispatch] = useStore('todoList');
// Let's ref the input to make it disabled while submit is being handled
const input = React.useRef(null);
const inputRef = React.useRef(null);

const onSubmit = e => {
e.preventDefault();
const todo = input.current.value;
input.current.value = '';
dispatch({ type: 'add', payload: todo }, todoCreated);
};

const todoCreated = newState => {
input.current.disabled = false;
input.current.value = '';
const todo = inputRef.current.value;
inputRef.current.value = '';
dispatch({ type: 'add', payload: todo });
};

return (
<form onSubmit={onSubmit}>
<input ref={input} />
<input ref={inputRef} />
<button>Create TODO</button>
</form>
);
Expand All @@ -177,7 +169,7 @@ function TodoList() {
export { TodoList, AddTodo };
```
## Methods API
### <a name="api_createStore">`createStore(name:String, state:*, reducer:Function):StoreInterface`</a>
### <a name="api_createStore">`createStore(name:String, state?:*, reducer?:Function):StoreInterface`</a>
Creates a store to be used across the entire application. Returns a StoreInterface object.
### Arguments
#### `name:String`
Expand All @@ -201,10 +193,14 @@ The store instance that is returned by the createStore and getStoreByName method
The name of the store;
#### `getState:Function():*`
A method that returns the store's current state
#### `setState:Function(state:*, callback:Function)`
#### `setState:Function(state:*, callback?:Function)`
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 receive new state as argument
#### `dispatch:Function(action:*, callback:Function)`
#### `dispatch:Function(action:*, callback?:Function)`
Dispatchs 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 receive new state as argument
#### `subscribe:Function(callback:Function):unsubscribe:Function`
subscribe a function to be called everytime a 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.

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>
Expand Down
8 changes: 7 additions & 1 deletion example/index.js
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { StatefulHello, AnotherComponent } from './basic';
import { AddTodo, TodoList } from './reducers';
import SubscriptionExample from './subscribe';

document.addEventListener('DOMContentLoaded', function() {
ReactDOM.render(
Expand All @@ -10,10 +11,15 @@ document.addEventListener('DOMContentLoaded', function() {
<p>A simple store without reducer logic</p>
<StatefulHello />
<AnotherComponent />
<hr/>
<h1>Advanced example</h1>
<p>A namespace and reducer-powered store, using callback</p>
<p>A reducer-powered store</p>
<AddTodo />
<TodoList />
<hr/>
<h1>Subscribe example</h1>
<p>Get notified when the state changes!</p>
<SubscriptionExample />
</React.Fragment>,
document.querySelector('#app')
);
Expand Down
21 changes: 7 additions & 14 deletions example/reducers.js
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import { createStore, useStore } from '../src';

// this one is more complex, it has a name and a reducer function
Expand Down Expand Up @@ -33,25 +33,18 @@ createStore(

export function AddTodo() {
const [ state, dispatch ] = useStore('todoList');
let input;
const inputRef = useRef(null);

const onSubmit = (e) => {
e.preventDefault();
input = e.target.querySelector('input');
const todo = input.value;
input.value = '.............';
input.disabled = true;
dispatch({ type: 'create', payload: todo },todoCreated);
const todo = inputRef.current.value;
inputRef.current.value = '';
dispatch({ type: 'create', payload: todo });
}

const todoCreated=(newState)=>{
input.disabled = false;
input.value = '';
}

return (
<form onSubmit={onSubmit}>
<input></input>
<input ref={inputRef}></input>
<button>Create TODO</button>
</form>
)
Expand Down
31 changes: 31 additions & 0 deletions example/subscribe.js
@@ -0,0 +1,31 @@
import React from 'react';
import { createStore, useStore } from '../src';

const defaultStyles = {
padding: 10, backgroundColor: 'turquoise', marginTop: 10, color: 'black'
}

const store = createStore('clickCounter2', 0);

// this will execute everytime the state is updated
const unsubscribe = store.subscribe((state) => {
alert('You increased the counter!');
if (state >= 3) {
//after three executions, lets unsubscribe to get rid of the alert!
unsubscribe();
}
})

export default function SubscriptionExample() {
const [ state, setState ] = useStore(store);

return (
<div style={{ ...defaultStyles }}>
<h1>Hello, component!</h1>
<h2>The button inside this component was clicked {state} times</h2>
<p> After 3 clicks, you won't receive anymore alerts! </p>
<button type="button" onClick={() => setState(state+1)}>Update</button>
</div>
);
}

62 changes: 19 additions & 43 deletions src/index.js
Expand Up @@ -11,8 +11,21 @@ class StoreInterface {
useReducer ?
this.dispatch = store.setState : this.setState = store.setState;
this.getState = () => store.state;
this.subscribe = subscribe;
this.unsubscribe = unsubscribe;
this.subscribe = this.subscribe.bind(this);
}

subscribe(callback) {
if (!callback || typeof callback !== 'function') {
throw `store.subscribe callback argument must be a function. got '${typeof callback}' instead.`;
}
if (subscriptions[this.name].find(c => c === callback)) {
console.warn('This callback is already subscribed to this store. skipping subscription');
return;
}
subscriptions[this.name].push(callback);
return () => {
subscriptions[this.name] = subscriptions[this.name].filter(c => c !== callback);
}
}

setState() {
Expand Down Expand Up @@ -55,17 +68,15 @@ export function createStore(name, state = {}, reducer=defaultReducer) {
setState(action, callback) {
this.state = this.reducer(this.state, action);
this.setters.forEach(setter => setter(this.state));
if (typeof callback === 'function') callback(this.state)
if (action && action.type &&
subscriptions[action.type]) {
subscriptions[action.type]
.forEach(subscription => subscription.name === name &&
subscription.callback(action, this.state));
if (subscriptions[name].length) {
subscriptions[name].forEach(c => c(this.state, action));
}
if (typeof callback === 'function') callback(this.state)
},
setters: []
};
store.setState = store.setState.bind(store);
subscriptions[name] = [];
store.public = new StoreInterface(name, store, reducer !== defaultReducer);

stores = Object.assign({}, stores, { [name]: store });
Expand Down Expand Up @@ -110,38 +121,3 @@ export function useStore(identifier) {

return [ state, store.setState ];
}

function subscribe(actions, callback) {
if (!actions || !Array.isArray(actions))
throw 'first argument must be an array';
if (!callback || typeof callback !== 'function')
throw 'second argument must be a function';
if( subscriberExists(this.name))
throw 'you are already subscribing to this store. unsubscribe to configure a new subscription.';
actions.forEach(action => {
if(!subscriptions[action]) {
subscriptions[action] = [];
}
subscriptions[action].push({callback, name:this.name});
});
}

function unsubscribe() {
const keys = Object.keys(subscriptions);
keys
.forEach(key => {
if(subscriptions[key].length === 1) {
delete subscriptions[key];
} else {
subscriptions[key] = subscriptions[key]
.filter((action, i) =>  action.name !== this.name);
}
});
};

function subscriberExists(name) {
const keys = Object.keys(subscriptions);
return keys.find(key => subscriptions[key]
.find(action => action && action.name === name)
);
}
6 changes: 3 additions & 3 deletions src/tests/store.test.js
Expand Up @@ -5,12 +5,12 @@ describe('createStore', () => {
const store = createStore('store1', 0);
expect(store.getState()).toBe(0);
expect(store.name).toBe('store1');
expect(Object.keys(store)).toEqual(['name', 'setState', 'getState', 'subscribe', 'unsubscribe']);
expect(Object.keys(store)).toEqual(['name', 'setState', 'getState', 'subscribe']);

const store2 = createStore('store2', 0, (state, action) => action.payload);
expect(store2.getState()).toBe(0);
expect(store2.name).toBe('store2');
expect(Object.keys(store2)).toEqual(['name', 'dispatch', 'getState', 'subscribe', 'unsubscribe']);
expect(Object.keys(store2)).toEqual(['name', 'dispatch', 'getState', 'subscribe']);
});

it('Should not allow stores with the same name to be created', () => {
Expand All @@ -33,7 +33,7 @@ describe('getStoreByName', () => {
createStore('test');

const store = getStoreByName('test');
expect(Object.keys(store)).toEqual(['name', 'setState', 'getState', 'subscribe', 'unsubscribe']);
expect(Object.keys(store)).toEqual(['name', 'setState', 'getState', 'subscribe']);
expect(store.name).toBe('test');
})

Expand Down

0 comments on commit cfb7225

Please sign in to comment.