Comparing version 0.3.0 to 0.4.0-0
@@ -1,3 +0,62 @@ | ||
export { useStore } from "./hooks"; | ||
export { makeStore } from "./store"; | ||
export * from "./types"; | ||
/** | ||
* The state objects managed by Statery stores are any JavaScript objects that | ||
* can be indexed using strings and/or numbers. | ||
*/ | ||
export declare type State = Record<string | number, any>; | ||
/** | ||
* Statery stores wrap around a State object and provide a few functions to update them | ||
* and, in turn, subscribe to updates. | ||
*/ | ||
export declare type Store<T extends State> = { | ||
/** | ||
* Return the current state. | ||
*/ | ||
state: T; | ||
/** | ||
* Updates the store. Accepts an object that will be (shallow-)merged into the current state, | ||
* or a callback that will be invoked with the current state and is expected to return an object | ||
* containing updates. | ||
* | ||
* Returns the updated version of the store. | ||
* | ||
* @example | ||
* store.set({ foo: 1 }) | ||
* | ||
* @example | ||
* store.set(state => ({ foo: state.foo + 1})) | ||
* | ||
* @see StateUpdateFunction | ||
*/ | ||
set: (updates: Partial<T> | StateUpdateFunction<T>) => T; | ||
/** | ||
* Subscribe to changes to the store's state. Every time the store is updated, the provided | ||
* listener callback will be invoked, with the object containing the updates passed as the | ||
* first argument, and the previous state as the second. | ||
* | ||
* @see Listener | ||
*/ | ||
subscribe: (listener: Listener<T>) => void; | ||
/** | ||
* Unsubscribe a listener from being invoked when the the store changes. | ||
*/ | ||
unsubscribe: (listener: Listener<T>) => void; | ||
}; | ||
export declare type StateUpdateFunction<T extends State> = (state: T) => Partial<T>; | ||
/** | ||
* A callback that can be passed to a store's `subscribe` and `unsubscribe` functions. | ||
*/ | ||
export declare type Listener<T extends State> = (updates: Partial<T>, state: T) => void; | ||
/** | ||
* Creates a Statery store and populates it with an initial state. | ||
* | ||
* @param state The state object that will be wrapped by the store. | ||
*/ | ||
export declare const makeStore: <T extends Record<string | number, any>>(initialState: T) => Store<T>; | ||
/** | ||
* Provides reactive read access to a Statery store. Returns a proxy object that | ||
* provides direct access to the store's state and makes sure that the React component | ||
* it was invoked from automaticaly re-renders when any of the data it uses is updated. | ||
* | ||
* @param store The Statery store to access. | ||
*/ | ||
export declare const useStore: <T extends Record<string | number, any>>(store: Store<T>) => T; |
@@ -1,51 +0,90 @@ | ||
import { useState, useEffect } from 'react'; | ||
import { useState, useRef, useEffect } from 'react'; | ||
const useStore = (store) => { | ||
return new Proxy({}, { | ||
get: (cache, prop) => { | ||
/* Memoize store access */ | ||
if (!cache.hasOwnProperty(prop)) | ||
cache[prop] = useStoreProperty(store, prop); | ||
return cache[prop]; | ||
/* | ||
▄████████ ███ ▄██████▄ ▄████████ ▄████████ | ||
███ ███ ▀█████████▄ ███ ███ ███ ███ ███ ███ | ||
███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ █▀ | ||
███ ███ ▀ ███ ███ ▄███▄▄▄▄██▀ ▄███▄▄▄ | ||
▀███████████ ███ ███ ███ ▀▀███▀▀▀▀▀ ▀▀███▀▀▀ | ||
███ ███ ███ ███ ▀███████████ ███ █▄ | ||
▄█ ███ ███ ███ ███ ███ ███ ███ ███ | ||
▄████████▀ ▄████▀ ▀██████▀ ███ ███ ██████████ | ||
███ ███ | ||
*/ | ||
/** | ||
* Creates a Statery store and populates it with an initial state. | ||
* | ||
* @param state The state object that will be wrapped by the store. | ||
*/ | ||
const makeStore = (initialState) => { | ||
let state = initialState; | ||
let listeners = new Array(); | ||
return { | ||
get state() { | ||
return state; | ||
}, | ||
set: (updates) => { | ||
/* Get new properties */ | ||
updates = updates instanceof Function ? updates(state) : updates; | ||
/* Execute listeners */ | ||
for (const listener of listeners) | ||
listener(updates, state); | ||
/* Apply updates */ | ||
state = Object.assign(Object.assign({}, state), updates); | ||
return state; | ||
}, | ||
subscribe: (listener) => { | ||
listeners.push(listener); | ||
}, | ||
unsubscribe: (listener) => { | ||
listeners = listeners.filter((l) => l !== listener); | ||
} | ||
}); | ||
}; | ||
}; | ||
const useStoreProperty = (store, prop) => { | ||
/* | ||
▄█ █▄ ▄██████▄ ▄██████▄ ▄█ ▄█▄ ▄████████ | ||
███ ███ ███ ███ ███ ███ ███ ▄███▀ ███ ███ | ||
███ ███ ███ ███ ███ ███ ███▐██▀ ███ █▀ | ||
▄███▄▄▄▄███▄▄ ███ ███ ███ ███ ▄█████▀ ███ | ||
▀▀███▀▀▀▀███▀ ███ ███ ███ ███ ▀▀█████▄ ▀███████████ | ||
███ ███ ███ ███ ███ ███ ███▐██▄ ███ | ||
███ ███ ███ ███ ███ ███ ███ ▀███▄ ▄█ ███ | ||
███ █▀ ▀██████▀ ▀██████▀ ███ ▀█▀ ▄████████▀ | ||
▀ | ||
*/ | ||
/** | ||
* Provides reactive read access to a Statery store. Returns a proxy object that | ||
* provides direct access to the store's state and makes sure that the React component | ||
* it was invoked from automaticaly re-renders when any of the data it uses is updated. | ||
* | ||
* @param store The Statery store to access. | ||
*/ | ||
const useStore = (store) => { | ||
/* A cheap version state that we will bump in order to re-render the component. */ | ||
const [, setVersion] = useState(0); | ||
/* A set containing all props that we're interested in. */ | ||
const interestingProps = useRef(new Set()).current; | ||
/* Subscribe to changes in the store. */ | ||
useEffect(() => { | ||
const listener = () => setVersion((v) => v + 1); | ||
/* On mounting, subscribe to the listener. */ | ||
store.subscribe(prop, listener); | ||
/* On unmounting, unsubscribe from it again. */ | ||
return () => void store.unsubscribe(prop, listener); | ||
}, [store, prop]); | ||
/* Return the requested property from our state. */ | ||
return store.state[prop]; | ||
}; | ||
const makeStore = (state) => { | ||
const listeners = {}; | ||
const set = (updates) => { | ||
var _a; | ||
/* Update state */ | ||
const newProps = updates instanceof Function ? updates(state) : updates; | ||
/* Execute listeners */ | ||
for (const prop in newProps) { | ||
const newValue = newProps[prop]; | ||
const prevValue = state[prop]; | ||
Object.assign(state, { [prop]: newValue }); | ||
(_a = listeners[prop]) === null || _a === void 0 ? void 0 : _a.forEach((listener) => { | ||
listener(newValue, prevValue); | ||
}); | ||
const listener = (updates) => { | ||
/* If there is at least one prop being updated that we're interested in, | ||
bump our local version. */ | ||
if (Object.keys(updates).find((prop) => interestingProps.has(prop))) { | ||
setVersion((v) => v + 1); | ||
} | ||
}; | ||
/* Mount & unmount the listener */ | ||
store.subscribe(listener); | ||
return () => void store.unsubscribe(listener); | ||
}, [store]); | ||
return new Proxy({}, { | ||
get: (_, prop) => { | ||
/* Add the prop we're interested in to the list of props */ | ||
interestingProps.add(prop); | ||
/* Return the value of the property. */ | ||
return store.state[prop]; | ||
} | ||
}; | ||
const subscribe = (prop, listener) => { | ||
if (!listeners[prop]) | ||
listeners[prop] = []; | ||
listeners[prop].push(listener); | ||
}; | ||
const unsubscribe = (prop, listener) => { | ||
listeners[prop] = listeners[prop].filter((l) => l !== listener); | ||
}; | ||
return { set, subscribe, unsubscribe, state }; | ||
}); | ||
}; | ||
@@ -52,0 +91,0 @@ |
@@ -7,50 +7,89 @@ 'use strict'; | ||
const useStore = (store) => { | ||
return new Proxy({}, { | ||
get: (cache, prop) => { | ||
/* Memoize store access */ | ||
if (!cache.hasOwnProperty(prop)) | ||
cache[prop] = useStoreProperty(store, prop); | ||
return cache[prop]; | ||
/* | ||
▄████████ ███ ▄██████▄ ▄████████ ▄████████ | ||
███ ███ ▀█████████▄ ███ ███ ███ ███ ███ ███ | ||
███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ █▀ | ||
███ ███ ▀ ███ ███ ▄███▄▄▄▄██▀ ▄███▄▄▄ | ||
▀███████████ ███ ███ ███ ▀▀███▀▀▀▀▀ ▀▀███▀▀▀ | ||
███ ███ ███ ███ ▀███████████ ███ █▄ | ||
▄█ ███ ███ ███ ███ ███ ███ ███ ███ | ||
▄████████▀ ▄████▀ ▀██████▀ ███ ███ ██████████ | ||
███ ███ | ||
*/ | ||
/** | ||
* Creates a Statery store and populates it with an initial state. | ||
* | ||
* @param state The state object that will be wrapped by the store. | ||
*/ | ||
const makeStore = (initialState) => { | ||
let state = initialState; | ||
let listeners = new Array(); | ||
return { | ||
get state() { | ||
return state; | ||
}, | ||
set: (updates) => { | ||
/* Get new properties */ | ||
updates = updates instanceof Function ? updates(state) : updates; | ||
/* Execute listeners */ | ||
for (const listener of listeners) | ||
listener(updates, state); | ||
/* Apply updates */ | ||
state = Object.assign(Object.assign({}, state), updates); | ||
return state; | ||
}, | ||
subscribe: (listener) => { | ||
listeners.push(listener); | ||
}, | ||
unsubscribe: (listener) => { | ||
listeners = listeners.filter((l) => l !== listener); | ||
} | ||
}); | ||
}; | ||
}; | ||
const useStoreProperty = (store, prop) => { | ||
/* | ||
▄█ █▄ ▄██████▄ ▄██████▄ ▄█ ▄█▄ ▄████████ | ||
███ ███ ███ ███ ███ ███ ███ ▄███▀ ███ ███ | ||
███ ███ ███ ███ ███ ███ ███▐██▀ ███ █▀ | ||
▄███▄▄▄▄███▄▄ ███ ███ ███ ███ ▄█████▀ ███ | ||
▀▀███▀▀▀▀███▀ ███ ███ ███ ███ ▀▀█████▄ ▀███████████ | ||
███ ███ ███ ███ ███ ███ ███▐██▄ ███ | ||
███ ███ ███ ███ ███ ███ ███ ▀███▄ ▄█ ███ | ||
███ █▀ ▀██████▀ ▀██████▀ ███ ▀█▀ ▄████████▀ | ||
▀ | ||
*/ | ||
/** | ||
* Provides reactive read access to a Statery store. Returns a proxy object that | ||
* provides direct access to the store's state and makes sure that the React component | ||
* it was invoked from automaticaly re-renders when any of the data it uses is updated. | ||
* | ||
* @param store The Statery store to access. | ||
*/ | ||
const useStore = (store) => { | ||
/* A cheap version state that we will bump in order to re-render the component. */ | ||
const [, setVersion] = react.useState(0); | ||
/* A set containing all props that we're interested in. */ | ||
const interestingProps = react.useRef(new Set()).current; | ||
/* Subscribe to changes in the store. */ | ||
react.useEffect(() => { | ||
const listener = () => setVersion((v) => v + 1); | ||
/* On mounting, subscribe to the listener. */ | ||
store.subscribe(prop, listener); | ||
/* On unmounting, unsubscribe from it again. */ | ||
return () => void store.unsubscribe(prop, listener); | ||
}, [store, prop]); | ||
/* Return the requested property from our state. */ | ||
return store.state[prop]; | ||
}; | ||
const makeStore = (state) => { | ||
const listeners = {}; | ||
const set = (updates) => { | ||
var _a; | ||
/* Update state */ | ||
const newProps = updates instanceof Function ? updates(state) : updates; | ||
/* Execute listeners */ | ||
for (const prop in newProps) { | ||
const newValue = newProps[prop]; | ||
const prevValue = state[prop]; | ||
Object.assign(state, { [prop]: newValue }); | ||
(_a = listeners[prop]) === null || _a === void 0 ? void 0 : _a.forEach((listener) => { | ||
listener(newValue, prevValue); | ||
}); | ||
const listener = (updates) => { | ||
/* If there is at least one prop being updated that we're interested in, | ||
bump our local version. */ | ||
if (Object.keys(updates).find((prop) => interestingProps.has(prop))) { | ||
setVersion((v) => v + 1); | ||
} | ||
}; | ||
/* Mount & unmount the listener */ | ||
store.subscribe(listener); | ||
return () => void store.unsubscribe(listener); | ||
}, [store]); | ||
return new Proxy({}, { | ||
get: (_, prop) => { | ||
/* Add the prop we're interested in to the list of props */ | ||
interestingProps.add(prop); | ||
/* Return the value of the property. */ | ||
return store.state[prop]; | ||
} | ||
}; | ||
const subscribe = (prop, listener) => { | ||
if (!listeners[prop]) | ||
listeners[prop] = []; | ||
listeners[prop].push(listener); | ||
}; | ||
const unsubscribe = (prop, listener) => { | ||
listeners[prop] = listeners[prop].filter((l) => l !== listener); | ||
}; | ||
return { set, subscribe, unsubscribe, state }; | ||
}); | ||
}; | ||
@@ -57,0 +96,0 @@ |
@@ -8,3 +8,4 @@ { | ||
}, | ||
"version": "0.3.0", | ||
"description": "A happy little state management library for React and friends.", | ||
"version": "0.4.0-0", | ||
"main": "dist/index.js", | ||
@@ -11,0 +12,0 @@ "module": "dist/index.esm.js", |
119
README.md
# STATERY | ||
![Status](https://img.shields.io/badge/status-experimental-orange) | ||
[![Build Size](https://img.shields.io/bundlephobia/min/statery?label=bundle%20size)](https://bundlephobia.com/result?p=statery) | ||
[![Version](https://img.shields.io/npm/v/statery)](https://www.npmjs.com/package/statery) | ||
[![Downloads](https://img.shields.io/npm/dt/statery.svg)](https://www.npmjs.com/package/statery) | ||
An extremely simple and just as experimental state management library for React. Yes, _another one_. Honestly, even I don't know why this exists. I had an idea and just _had_ to try it, and yet another NPM package was born. | ||
@@ -9,8 +14,20 @@ | ||
## Demos | ||
## DEMOS | ||
- [Example Clicker Game](https://codesandbox.io/s/statery-clicker-game-hjxk3?file=/src/App.tsx) (Codesandbox) | ||
## Basic Usage | ||
## BASIC USAGE | ||
### Adding it to your Project | ||
``` | ||
npm install statery | ||
``` | ||
or | ||
``` | ||
yarn add statery | ||
``` | ||
### Creating a Store | ||
@@ -21,2 +38,4 @@ | ||
```ts | ||
import { makeStore } from "statery" | ||
const store = makeStore({ | ||
@@ -28,16 +47,2 @@ wood: 8, | ||
### Reading from a Store | ||
Within a React component, use the `useStore` hook to read data from the store: | ||
```tsx | ||
const Wood = () => { | ||
const { wood } = useStore(store) | ||
return <p>Wood: {wood}</p> | ||
} | ||
``` | ||
Naturally, your components will **re-render** when the data they've accessed changes. | ||
### Updating the Store | ||
@@ -69,4 +74,31 @@ | ||
## Advanced Usage | ||
Updates will be shallow-merged with the current state, meaning that properties you don't update will not be touched. | ||
### Reading from a Store (React) | ||
Within a React component, use the `useStore` hook to read data from the store: | ||
```tsx | ||
import { useStore } from "statery" | ||
const Wood = () => { | ||
const { wood } = useStore(store) | ||
return <p>Wood: {wood}</p> | ||
} | ||
``` | ||
Naturally, your components will **re-render automatically** when the data they've accessed changes. | ||
### Reading from a Store (outside of React) | ||
A Statery store provides access to its current state through its `state` property: | ||
```ts | ||
const store = makeStore({ count: 0 }) | ||
console.log(store.state.count) | ||
``` | ||
## ADVANCED USAGE | ||
### Deriving Values from a Store | ||
@@ -80,7 +112,15 @@ | ||
Due to the way Statery is designed, these will work both within mutation code... | ||
These will work from within plain imperative JavaScript code... | ||
```tsx | ||
if (canBuyHouse(store.state)) { | ||
console.log("Let's buy a house!") | ||
} | ||
``` | ||
...mutation code... | ||
```tsx | ||
const buyHouse = () => | ||
gameState.set((state) => | ||
store.set((state) => | ||
canBuyHouse(state) | ||
@@ -100,6 +140,6 @@ ? { | ||
const BuyHouseButton = () => { | ||
const store = useStore(gameState) | ||
const proxy = useStore(store) | ||
return ( | ||
<button onClick={buyHouse} disabled={!canBuyHouse(store)}> | ||
<button onClick={buyHouse} disabled={!canBuyHouse(proxy)}> | ||
Buy House (5g, 5w) | ||
@@ -111,26 +151,37 @@ </button> | ||
### Accessing the State Directly | ||
### Subscribing to updates (imperatively) | ||
If, for any reason, you ever need to work with the underlying state object without any potentially unwanted magic happening, you can use the store's `state` property to access it directly: | ||
Use a store's `subscribe` function to register a callback that will be executed every time the store is changed. | ||
The callback will receive both an object containing of the changes, as well as the store's current state. | ||
```ts | ||
const store = makeStore({ count: 0 }) | ||
console.log(store.state.count) | ||
const listener = (changes, state) => { | ||
console.log("Applying changes:", changes) | ||
} | ||
store.subscribe(console.log) | ||
store.set((state) => ({ count: state.count + 1 })) | ||
store.unsubscribe(console.log) | ||
``` | ||
**Note:** this won't stop you from mutating the state object. Keep in mind that when you do this, none of the subscribed listeners will be executed. | ||
### TypeScript support | ||
### Subscribing to updates imperatively | ||
Statery is written in TypeScript, and its stores are fully typed. If you're about to update a store with a property that it doesn't know about, TypeScript will warn you. If the state structure `makeStore` infers from its initial state argument is not good enough, you can explicitly pass a store type as `makeStore`'s type parameter: | ||
Use a store's `subscribe` function to register a callback that will be executed every time a specific property is changed. Listeners will be passed both the new as well as the previous value as arguments. | ||
```tsx | ||
const store = makeStore<{ name?: string }>({}) | ||
store.set({ name: "Statery" }) // ✅ All good | ||
store.set({ foo: 123 }) // 😭 TypeScript warnin | ||
``` | ||
```ts | ||
const store = makeStore({ count: 0 }) | ||
store.subscribe("count", console.log) | ||
## NOTES | ||
/* Now every time an update is made to the the store's "count" property, | ||
it will be logged to the console. */ | ||
### Stuff that probably needs work | ||
store.unsubscribe("count", console.log) | ||
``` | ||
- [ ] I have yet to try how well Statery works with async updating of the store. | ||
- [ ] Statery _probably_ has problems in React's Concurrent Mode. I haven't tried yet, but I will. | ||
- [ ] No support for middleware yet. Haven't decided on an API that is adequately simple. | ||
- [ ] Probably other bits and pieces I haven't even encountered yet. | ||
@@ -137,0 +188,0 @@ ### Motivation & Assumptions |
178
src/index.ts
@@ -1,3 +0,175 @@ | ||
export { useStore } from "./hooks" | ||
export { makeStore } from "./store" | ||
export * from "./types" | ||
import { useEffect, useRef, useState } from "react" | ||
/* | ||
███ ▄██ ▄ ▄███████▄ ▄████████ ▄████████ | ||
▀█████████▄ ███ ██▄ ███ ███ ███ ███ ███ ███ | ||
▀███▀▀██ ███▄▄▄███ ███ ███ ███ █▀ ███ █▀ | ||
███ ▀ ▀▀▀▀▀▀███ ███ ███ ▄███▄▄▄ ███ | ||
███ ▄██ ███ ▀█████████▀ ▀▀███▀▀▀ ▀███████████ | ||
███ ███ ███ ███ ███ █▄ ███ | ||
███ ███ ███ ███ ███ ███ ▄█ ███ | ||
▄████▀ ▀█████▀ ▄████▀ ██████████ ▄████████▀ | ||
*/ | ||
/** | ||
* The state objects managed by Statery stores are any JavaScript objects that | ||
* can be indexed using strings and/or numbers. | ||
*/ | ||
export type State = Record<string | number, any> | ||
/** | ||
* Statery stores wrap around a State object and provide a few functions to update them | ||
* and, in turn, subscribe to updates. | ||
*/ | ||
export type Store<T extends State> = { | ||
/** | ||
* Return the current state. | ||
*/ | ||
state: T | ||
/** | ||
* Updates the store. Accepts an object that will be (shallow-)merged into the current state, | ||
* or a callback that will be invoked with the current state and is expected to return an object | ||
* containing updates. | ||
* | ||
* Returns the updated version of the store. | ||
* | ||
* @example | ||
* store.set({ foo: 1 }) | ||
* | ||
* @example | ||
* store.set(state => ({ foo: state.foo + 1})) | ||
* | ||
* @see StateUpdateFunction | ||
*/ | ||
set: (updates: Partial<T> | StateUpdateFunction<T>) => T | ||
/** | ||
* Subscribe to changes to the store's state. Every time the store is updated, the provided | ||
* listener callback will be invoked, with the object containing the updates passed as the | ||
* first argument, and the previous state as the second. | ||
* | ||
* @see Listener | ||
*/ | ||
subscribe: (listener: Listener<T>) => void | ||
/** | ||
* Unsubscribe a listener from being invoked when the the store changes. | ||
*/ | ||
unsubscribe: (listener: Listener<T>) => void | ||
} | ||
export type StateUpdateFunction<T extends State> = (state: T) => Partial<T> | ||
/** | ||
* A callback that can be passed to a store's `subscribe` and `unsubscribe` functions. | ||
*/ | ||
export type Listener<T extends State> = (updates: Partial<T>, state: T) => void | ||
/* | ||
▄████████ ███ ▄██████▄ ▄████████ ▄████████ | ||
███ ███ ▀█████████▄ ███ ███ ███ ███ ███ ███ | ||
███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ █▀ | ||
███ ███ ▀ ███ ███ ▄███▄▄▄▄██▀ ▄███▄▄▄ | ||
▀███████████ ███ ███ ███ ▀▀███▀▀▀▀▀ ▀▀███▀▀▀ | ||
███ ███ ███ ███ ▀███████████ ███ █▄ | ||
▄█ ███ ███ ███ ███ ███ ███ ███ ███ | ||
▄████████▀ ▄████▀ ▀██████▀ ███ ███ ██████████ | ||
███ ███ | ||
*/ | ||
/** | ||
* Creates a Statery store and populates it with an initial state. | ||
* | ||
* @param state The state object that will be wrapped by the store. | ||
*/ | ||
export const makeStore = <T extends State>(initialState: T): Store<T> => { | ||
let state = initialState | ||
let listeners = new Array<Listener<T>>() | ||
return { | ||
get state() { | ||
return state | ||
}, | ||
set: (updates) => { | ||
/* Get new properties */ | ||
updates = updates instanceof Function ? updates(state) : updates | ||
/* Execute listeners */ | ||
for (const listener of listeners) listener(updates, state) | ||
/* Apply updates */ | ||
state = { ...state, ...updates } | ||
return state | ||
}, | ||
subscribe: (listener) => { | ||
listeners.push(listener) | ||
}, | ||
unsubscribe: (listener) => { | ||
listeners = listeners.filter((l) => l !== listener) | ||
} | ||
} | ||
} | ||
/* | ||
▄█ █▄ ▄██████▄ ▄██████▄ ▄█ ▄█▄ ▄████████ | ||
███ ███ ███ ███ ███ ███ ███ ▄███▀ ███ ███ | ||
███ ███ ███ ███ ███ ███ ███▐██▀ ███ █▀ | ||
▄███▄▄▄▄███▄▄ ███ ███ ███ ███ ▄█████▀ ███ | ||
▀▀███▀▀▀▀███▀ ███ ███ ███ ███ ▀▀█████▄ ▀███████████ | ||
███ ███ ███ ███ ███ ███ ███▐██▄ ███ | ||
███ ███ ███ ███ ███ ███ ███ ▀███▄ ▄█ ███ | ||
███ █▀ ▀██████▀ ▀██████▀ ███ ▀█▀ ▄████████▀ | ||
▀ | ||
*/ | ||
/** | ||
* Provides reactive read access to a Statery store. Returns a proxy object that | ||
* provides direct access to the store's state and makes sure that the React component | ||
* it was invoked from automaticaly re-renders when any of the data it uses is updated. | ||
* | ||
* @param store The Statery store to access. | ||
*/ | ||
export const useStore = <T extends State>(store: Store<T>): T => { | ||
/* A cheap version state that we will bump in order to re-render the component. */ | ||
const [, setVersion] = useState(0) | ||
/* A set containing all props that we're interested in. */ | ||
const interestingProps = useRef(new Set<keyof T>()).current | ||
/* Subscribe to changes in the store. */ | ||
useEffect(() => { | ||
const listener: Listener<T> = (updates: Partial<T>) => { | ||
/* If there is at least one prop being updated that we're interested in, | ||
bump our local version. */ | ||
if (Object.keys(updates).find((prop) => interestingProps.has(prop))) { | ||
setVersion((v) => v + 1) | ||
} | ||
} | ||
/* Mount & unmount the listener */ | ||
store.subscribe(listener) | ||
return () => void store.unsubscribe(listener) | ||
}, [store]) | ||
return new Proxy<Record<any, any>>( | ||
{}, | ||
{ | ||
get: (_, prop: keyof T) => { | ||
/* Add the prop we're interested in to the list of props */ | ||
interestingProps.add(prop) | ||
/* Return the value of the property. */ | ||
return store.state[prop] | ||
} | ||
} | ||
) | ||
} |
@@ -1,15 +0,16 @@ | ||
import { makeStore } from "../src/store" | ||
import { Listener } from "../src/types" | ||
import { makeStore } from "../src" | ||
describe("makeStore", () => { | ||
const state = { | ||
const store = makeStore({ | ||
foo: 0, | ||
bar: 0 | ||
} | ||
}) | ||
const store = makeStore(state) | ||
beforeEach(() => { | ||
store.set({ foo: 0, bar: 0 }) | ||
}) | ||
describe(".state", () => { | ||
it("provides direct access to the state object", () => { | ||
expect(store.state).toBe(state) | ||
expect(store.state).toEqual({ foo: 0, bar: 0 }) | ||
}) | ||
@@ -21,51 +22,57 @@ }) | ||
store.set({ foo: 10 }) | ||
expect(state.foo).toEqual(10) | ||
expect(store.state.foo).toEqual(10) | ||
}) | ||
it("accepts a function that accepts the state and returns an update dictionary", () => { | ||
const current = state.foo | ||
const current = store.state.foo | ||
expect(store.state.foo).toEqual(current) | ||
store.set(({ foo }) => ({ foo: foo + 1 })) | ||
expect(state.foo).toEqual(current + 1) | ||
expect(store.state.foo).toEqual(current + 1) | ||
}) | ||
it("returns the updated state", () => { | ||
const result = store.set({ foo: 1 }) | ||
expect(result).toEqual({ foo: 1, bar: 0 }) | ||
}) | ||
}) | ||
describe(".subscribe", () => { | ||
it("allows subscribing to updates to a specific property of the store", () => { | ||
let fooChanges = 0 | ||
let barChanges = 0 | ||
it("accepts a listener callback that will be invoked when the store changes", () => { | ||
const listener = jest.fn() | ||
store.set({ foo: 0, bar: 0 }) | ||
store.subscribe(listener) | ||
store.set({ foo: 1 }) | ||
store.unsubscribe(listener) | ||
const fooListener = () => fooChanges++ | ||
const barListener = () => barChanges++ | ||
/* It should have been called exactly once */ | ||
expect(listener.mock.calls.length).toBe(1) | ||
store.subscribe("foo", fooListener) | ||
store.subscribe("bar", barListener) | ||
/* The first argument should be the changes */ | ||
expect(listener.mock.calls[0][0]).toEqual({ foo: 1 }) | ||
store.set(({ foo }) => ({ foo: foo + 1 })) | ||
store.set(({ foo, bar }) => ({ foo: foo + 1, bar: bar + 1 })) | ||
store.unsubscribe("foo", fooListener) | ||
store.unsubscribe("bar", barListener) | ||
expect(fooChanges).toEqual(2) | ||
expect(barChanges).toEqual(1) | ||
/* The second argument should be the previous state */ | ||
expect(listener.mock.calls[0][1]).toEqual({ foo: 0, bar: 0 }) | ||
}) | ||
it("feeds the changed values to the listener callback", () => { | ||
let newFoo: number | ||
let prevFoo: number | ||
it("allows subscribing to updates to a store", () => { | ||
const changeCounters = { | ||
foo: 0, | ||
bar: 0 | ||
} | ||
const listener: Listener<number> = (newValue, prevValue) => { | ||
newFoo = newValue | ||
prevFoo = prevValue | ||
const listener = (updates) => { | ||
for (const prop in updates) changeCounters[prop]++ | ||
} | ||
store.set({ foo: 0 }) | ||
store.subscribe("foo", listener) | ||
store.set({ foo: 1 }) | ||
store.unsubscribe("foo", listener) | ||
store.subscribe(listener) | ||
expect(newFoo).toBe(1) | ||
expect(prevFoo).toBe(0) | ||
store.set(({ foo }) => ({ foo: foo + 1 })) | ||
store.set(({ foo, bar }) => ({ foo: foo + 1, bar: bar + 1 })) | ||
store.unsubscribe(listener) | ||
expect(changeCounters.foo).toEqual(2) | ||
expect(changeCounters.bar).toEqual(1) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
200286
611
196
17
1