├── .gitignore ├── README.md ├── demo ├── .npmignore ├── Calculate.tsx ├── Clock.tsx ├── WithWorker.tsx ├── WithoutWorker.tsx ├── index.html ├── index.tsx ├── lib │ └── primes.ts ├── package.json ├── redux │ ├── actions.ts │ ├── reducer.ts │ ├── store.ts │ └── worker.ts ├── tsconfig.json └── yarn.lock ├── img ├── react-redux-worker.svg └── worker-demo.gif ├── package.json ├── src ├── ProxyStore.ts ├── StateContext.tsx ├── StoreContext.tsx ├── createProxyStore.ts ├── expose.ts ├── getProvider.tsx ├── hooks.ts ├── index.ts └── uniqueId.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | .rts2_cache_cjs 6 | .rts2_cache_esm 7 | .rts2_cache_umd 8 | dist 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-worker 2 | 3 | Run a Redux store in a web worker. 4 | 5 | ## Why? 6 | 7 | If you're doing any sort of computationally expensive work in your [Redux](https://redux.js.org) reducers or middleware, it 8 | can prevent your UI from responding while it's thinking—making your application feel slow and 9 | unresponsive. 10 | 11 | In theory, web workers should be the perfect solution: You can do your heavy lifting in a worker 12 | thread without interfering with your main UI thread. But the message-based [web worker 13 | API](https://redux.js.org) puts us on unfamiliar terrain. 14 | 15 | This library is intended to make the developer experience of using a worker-based Redux store as 16 | similar as possible to an ordinary Redux setup. 17 | 18 | ## How it works 19 | 20 | This library provides you with a **proxy Redux store**. To your application, the proxy looks just 21 | like the real thing: You communicate with it synchronously using `useDispatch` and `useSelector` 22 | hooks just like the ones that the official [react-redux](https://github.com/reduxjs/react-redux) 23 | bindings provide. 24 | 25 | ![diagram](https://raw.githubusercontent.com/HerbCaudill/react-redux-worker/2c2665410d3de1e88c15c60c0a8c492d6dd72c10/img/react-redux-worker.svg?sanitize=true) 26 | 27 | The proxy then handles messaging back and forth with the store in the worker using the 28 | [Comlink](https://github.com/GoogleChromeLabs/comlink) library, built by the Google Chrome team. 29 | 30 | ## Running the demo 31 | 32 | ```bash 33 | yarn 34 | yarn start 35 | ``` 36 | 37 | Then open http://localhost:1234 in a browser. You should see something like this: 38 | 39 | ![demo](https://github.com/HerbCaudill/react-redux-worker/raw/2c2665410d3de1e88c15c60c0a8c492d6dd72c10/img/worker-demo.gif) 40 | 41 | ## Usage 42 | 43 | ### Add the dependency 44 | 45 | ```bash 46 | yarn add react-redux-worker 47 | ``` 48 | 49 | ### Put your store in a worker, and create a proxy 50 | 51 | In a stand-alone file called `worker.ts` or `store.worker.ts`, import your reducer (and middlewares, 52 | if applicable) and build your store the way you always have. Then wrap it in a proxy store, 53 | and expose that as a worker messaging endpoint: 54 | 55 | ```ts 56 | // worker.ts 57 | import { createStore } from 'redux' 58 | import { reducer } from './reducer' 59 | import { expose, createProxyStore } from 'react-redux-worker' 60 | 61 | const store = createStore(reducer) // if you have initial state and/or middleware you can add them here as well 62 | const proxyStore = createProxyStore(store) 63 | expose(proxyStore, self) 64 | ``` 65 | 66 | ### Add a context provider for the proxy store 67 | 68 | At the root of your app, replace your standard react-redux `Provider` with one that gives access to 69 | the proxy store. 70 | 71 | ```tsx 72 | import { getProvider } from 'react-redux-worker' 73 | 74 | const worker = new Worker('./redux/worker.ts') 75 | const ProxyProvider = getProvider(worker) 76 | 77 | ReactDOM.render( 78 | 79 | 80 | , 81 | document.querySelector('.root') 82 | ) 83 | ``` 84 | 85 | ### Use the proxy `useDispatch` and `useSelector` hooks in your components 86 | 87 | ```tsx 88 | import * as React from 'react' 89 | import { useDispatch, useSelector } from 'react-redux-worker' 90 | import { actions } from './redux/actions' 91 | 92 | export function WithWorker() { 93 | const state = useSelector((s => s) 94 | const dispatch = useDispatch() 95 | 96 | dispatch(actions.setBusy(true)) 97 | dispatch(actions.doSomeHeavyLifting()) 98 | dispatch(actions.setBusy(false)) 99 | 100 | return (
101 | {state.busy ? ( 102 | Thinking... 103 | ) : ( 104 | Result: {state.result} 105 | )} 106 |
) 107 | } 108 | ``` 109 | 110 | ## Prior art 111 | 112 | - Based on [redux-workerized](https://github.com/mizchi/redux-workerized) by 113 | [@mizchi](https://github.com/mizchi/) 114 | - Uses some ideas from [A Guide to using Web Workers in 115 | React](https://www.fullstackreact.com/articles/introduction-to-web-workers-with-react) by [@yomieluwande](https://twitter.com/yomieluwande) 116 | -------------------------------------------------------------------------------- /demo/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /demo/Calculate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { actions } from './redux/actions' 3 | import { State } from './redux/reducer' 4 | import { Dispatch } from 'redux' 5 | 6 | // When doing calculations in UI thread, React won't update at all if we don't do this 7 | export const nextFrame = () => new Promise(ok => requestAnimationFrame(ok)) 8 | 9 | export const Calculate = ({ 10 | state, 11 | dispatch, 12 | }: { 13 | state: State 14 | dispatch: Dispatch 15 | }) => { 16 | const calculate = async () => { 17 | dispatch(actions.setBusy(true)) 18 | await nextFrame() 19 | for (let i = 0; i < 10; i++) { 20 | dispatch(actions.nextPrime()) 21 | await nextFrame() 22 | } 23 | dispatch(actions.setBusy(false)) 24 | } 25 | const style = { 26 | button: { 27 | border: '2px solid', 28 | margin: '10px 0', 29 | padding: '10px 30px', 30 | borderRadius: '10px', 31 | cursor: 'pointer', 32 | outline: 'none', 33 | minWidth: '10em', 34 | ...(state.busy 35 | ? { 36 | background: '#008080bb', 37 | borderColor: '#008080', 38 | color: 'white', 39 | } 40 | : { 41 | background: '#00808022', 42 | borderColor: '#008080', 43 | color: '#008080', 44 | }), 45 | }, 46 | } 47 | return ( 48 |
49 | 52 | {state.primes.map(p => ( 53 |
{p}
54 | ))} 55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /demo/Clock.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useState } from 'react' 3 | import ReactCountdownClock from 'react-countdown-clock' 4 | 5 | export const Clock = () => { 6 | const [time, setTime] = useState(3) 7 | const [seconds, setSeconds] = useState(time) 8 | 9 | const restart = () => { 10 | // can't just set seconds back to the starting value, or it won't restart 11 | const t = time + 0.00000000001 12 | setTime(t) 13 | setSeconds(t) 14 | } 15 | 16 | return ( 17 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /demo/WithWorker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useDispatch, useSelector } from '../src' 3 | import { State } from './redux/reducer' 4 | import { Calculate } from './Calculate' 5 | 6 | export function WithWorker() { 7 | const state = useSelector((s: State) => s) 8 | const dispatch = useDispatch() 9 | 10 | return ( 11 |
12 |

With worker

13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /demo/WithoutWorker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { Calculate } from './Calculate' 4 | import { State } from './redux/reducer' 5 | 6 | export function WithoutWorker() { 7 | const state = useSelector((s: State) => s) 8 | const dispatch = useDispatch() 9 | 10 | return ( 11 |
12 |

Without worker

13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { store } from './redux/store' 5 | 6 | import { getProvider } from '../src' // replace with 'react-redux-worker' 7 | 8 | import { Clock } from './Clock' 9 | import { WithoutWorker } from './WithoutWorker' 10 | import { WithWorker } from './WithWorker' 11 | 12 | // Set up proxy provider 13 | const worker = new Worker('./redux/worker.ts') 14 | const ProxyProvider = getProvider(worker) 15 | 16 | // Set up regular provider 17 | const RegularProvider = ({ children }) => ( 18 | {children} 19 | ) 20 | 21 | ReactDOM.render( 22 |
23 | 24 |

Use the buttons to find a few large prime numbers.

25 |

26 | Without a worker, notice how the animation stops updating every time the 27 | app calculates a new prime. 28 |

29 |
30 | {/* using redux-workerized */} 31 | 32 | 33 | 34 | 35 | {/* using regular redux */} 36 | 37 | 38 | 39 |
40 |
, 41 | document.querySelector('.root') 42 | ) 43 | -------------------------------------------------------------------------------- /demo/lib/primes.ts: -------------------------------------------------------------------------------- 1 | export const knownPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] 2 | export const highestKnownPrime = () => knownPrimes[knownPrimes.length - 1] 3 | 4 | // returns an array of primes lower than `max` 5 | export const primes = (max = 100) => { 6 | let p = highestKnownPrime() 7 | while (p < max) { 8 | p = nextPrime(p) 9 | if (p > highestKnownPrime()) knownPrimes.push(p) 10 | } 11 | return knownPrimes.filter(p => p < max) 12 | } 13 | 14 | // returns the next prime after `n` 15 | export const nextPrime = (n: number): number => { 16 | // if it's in the range of the list, just look it up 17 | if (n < highestKnownPrime()) return knownPrimes.find(p => p > n) as number 18 | 19 | // brute-force time 20 | let candidates = candidateGenerator(n) 21 | let candidate 22 | do candidate = candidates.next().value 23 | while (!isPrime(candidate)) 24 | 25 | return candidate 26 | } 27 | 28 | export const nthPrime = (n: number): number => { 29 | let i = 1 30 | let p = 2 // 2 is the first prime 31 | while (i++ < n) p = nextPrime(p) 32 | return p 33 | } 34 | 35 | // uses the fact that every prime over 30 is in one of the forms 36 | // 30k ± 1, 30k ± 7, 30k ± 11, 30k ± 13 37 | // This allows us to eliminate more than half (11/15 = 73%) of the search space 38 | export const candidateGenerator = function*(n: number) { 39 | const B = 30 40 | const D = [-13, -11, -7, -1, 1, 7, 11, 13] 41 | let i = 0 42 | let base = Math.trunc(n / B) * B 43 | while (true) { 44 | let candidate = base + D[i] 45 | if (candidate > n) yield candidate 46 | i += 1 47 | if (i >= D.length) { 48 | base += B 49 | i = 0 50 | } 51 | } 52 | } 53 | 54 | // returns true if a number is prime, false if it is composite 55 | export const isPrime = (n: number) => { 56 | // negative numbers and zero are not prime 57 | if (n < 1) return false 58 | 59 | // if it's in the range of the list, then it's only prime if it's on the list 60 | const hnp = highestKnownPrime() 61 | if (n <= hnp) return knownPrimes.includes(n) 62 | 63 | const sqrt = Math.sqrt(n) 64 | 65 | // if it's divisible by a number on the list, it's not prime 66 | if (knownPrimes.find(p => n % p === 0)) return false 67 | // if it's not divisible by a number on the list and it's 68 | // smaller than the square of the largest known prime, then it's prime 69 | else if (sqrt < hnp) return true 70 | 71 | // Brute-force time 72 | let candidate = hnp 73 | let candidates = candidateGenerator(candidate) 74 | do { 75 | if (n % candidate === 0) return false 76 | candidate = candidates.next().value 77 | } while (candidate <= sqrt) 78 | 79 | // must be prime 80 | return true 81 | } 82 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-countdown-clock": "2", 12 | "react-redux": "7", 13 | "redux": "4" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "16", 17 | "@types/react-dom": "16", 18 | "@types/react-redux": "^7.1.4", 19 | "parcel": "1", 20 | "typescript": "3" 21 | }, 22 | "alias": { 23 | "react": "../node_modules/react", 24 | "react-dom": "../node_modules/react-dom/profiling", 25 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | 3 | // Constants 4 | 5 | export const NEXT_PRIME = 'counter/next-prime' 6 | export const SET_BUSY = 'counter/set-busy' 7 | 8 | export interface Action extends AnyAction {} 9 | 10 | export const actions: { [key: string]: (...args: any[]) => Action } = { 11 | nextPrime: () => ({ type: NEXT_PRIME }), 12 | setBusy: (value: boolean) => ({ type: SET_BUSY, payload: { value } }), 13 | } 14 | -------------------------------------------------------------------------------- /demo/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | import { NEXT_PRIME, SET_BUSY } from './actions' 3 | import { nextPrime } from '../lib/primes' 4 | 5 | export type State = { 6 | prime: number 7 | primes: number[] 8 | busy: boolean 9 | } 10 | 11 | const initialState = { 12 | prime: 861504408610801, 13 | primes: [], 14 | busy: false, 15 | } 16 | 17 | function reducer(state: State = initialState, action: AnyAction) { 18 | switch (action.type) { 19 | case NEXT_PRIME: { 20 | const next = nextPrime(state.prime) 21 | return { 22 | ...state, 23 | prime: next, 24 | primes: [...state.primes, next], 25 | } 26 | } 27 | case SET_BUSY: { 28 | const { value } = action.payload 29 | return { 30 | ...state, 31 | busy: value, 32 | } 33 | } 34 | default: 35 | return state 36 | } 37 | } 38 | 39 | export default reducer 40 | -------------------------------------------------------------------------------- /demo/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import reducer from './reducer' 3 | 4 | export const store = createStore(reducer) 5 | -------------------------------------------------------------------------------- /demo/redux/worker.ts: -------------------------------------------------------------------------------- 1 | import { expose, createProxyStore } from '../../src' // replace with react-redux-worker 2 | import { store } from './store' 3 | 4 | const proxyStore = createProxyStore(store) 5 | expose(proxyStore, self) 6 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["es2015", "es2016", "dom"], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noImplicitAny": false, 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "preserveConstEnums": true, 14 | "removeComments": true, 15 | "sourceMap": true, 16 | "strictNullChecks": true, 17 | "target": "es5", 18 | "types": ["node"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /img/react-redux-worker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/worker-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerbCaudill/react-redux-worker/69c40b983058ecdab68589aba2606a1a7cb03522/img/worker-demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-worker", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/HerbCaudill/react-redux-worker" 6 | }, 7 | "description": "Run a Redux store in a Web Worker", 8 | "keywords": [ 9 | "worker", 10 | "web-worker", 11 | "redux", 12 | "react" 13 | ], 14 | "version": "0.1.9", 15 | "license": "MIT", 16 | "main": "dist/index.js", 17 | "module": "dist/react-redux-worker.esm.js", 18 | "typings": "dist/index.d.ts", 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "dev": "tsdx watch", 24 | "build": "tsdx build", 25 | "test": "tsdx test --env=jsdom", 26 | "lint": "tsdx lint", 27 | "prepublish": "tsdx build", 28 | "start": "cd ./demo && yarn start" 29 | }, 30 | "dependencies": { 31 | "@types/lodash.isequal": "^4.5.5", 32 | "comlinkjs": "3", 33 | "lodash.isequal": "4" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "24", 37 | "@types/react": "16", 38 | "@types/react-dom": "16", 39 | "husky": "3", 40 | "react": "16", 41 | "react-dom": "16", 42 | "redux": "4", 43 | "tsdx": "0", 44 | "tslib": "1", 45 | "typescript": "3" 46 | }, 47 | "peerDependencies": { 48 | "react": ">=16", 49 | "redux": ">=4" 50 | }, 51 | "prettier": { 52 | "printWidth": 80, 53 | "semi": false, 54 | "singleQuote": true, 55 | "trailingComma": "es5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ProxyStore.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | 3 | export type ProxyStore = { 4 | getState(): Promise 5 | dispatch(action: A): Promise 6 | subscribe( 7 | listener: (state: State) => void, 8 | selector?: (root: State) => Promise | State 9 | ): Promise 10 | unsubscribe(listenerId: number): Promise 11 | } 12 | -------------------------------------------------------------------------------- /src/StateContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | export const StateContext = React.createContext(null as any) 3 | -------------------------------------------------------------------------------- /src/StoreContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | export const StoreContext = React.createContext(null as any) 3 | -------------------------------------------------------------------------------- /src/createProxyStore.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash.isequal' 2 | import { AnyAction, Store } from 'redux' 3 | import { ProxyStore } from './ProxyStore' 4 | import { uniqueId } from './uniqueId' 5 | 6 | /** 7 | * Since our store is running in a worker process, we provide our application with a proxy 8 | * store that looks to it just like a garden-variety Redux store, except that everything is 9 | * asynchronous, since all communication with the worker is based on handling message events. 10 | * 11 | * Once created, the proxy needs to be exposed using the `expose` function: 12 | * ```js 13 | * import {expose, createProxyStore} from '...' 14 | * const store = createStore(reducer) 15 | * const proxyStore = createProxyStore(store) 16 | * expose(proxyStore, self) 17 | *``` 18 | * @param store A regular Redux store created using `Redux.createStore`. 19 | */ 20 | export const createProxyStore = ( 21 | store: Store 22 | ): ProxyStore => { 23 | const listenerMap = new Map() 24 | return { 25 | async subscribe(onChangeHandler: Function): Promise { 26 | const subscriptionId = uniqueId() 27 | let lastSnapshot = store.getState() 28 | const unsubscribe = store.subscribe(async () => { 29 | const newSnapshot = store.getState() 30 | if (!isEqual(lastSnapshot, newSnapshot)) { 31 | onChangeHandler(newSnapshot) 32 | lastSnapshot = newSnapshot 33 | } 34 | }) 35 | listenerMap.set(subscriptionId, unsubscribe) 36 | return subscriptionId 37 | }, 38 | async unsubscribe(subscriptionId: number) { 39 | const listener = listenerMap.get(subscriptionId) 40 | if (listener) listener() 41 | listenerMap.delete(subscriptionId) 42 | }, 43 | async getState() { 44 | return store.getState() 45 | }, 46 | async dispatch(action: AnyAction) { 47 | store.dispatch(action) 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/expose.ts: -------------------------------------------------------------------------------- 1 | import { ProxyStore } from './ProxyStore' 2 | import * as Comlink from 'comlinkjs' 3 | 4 | /** 5 | * Uses `Comlink` to expose a `proxyStore` as a worker. 6 | * 7 | * Example: 8 | * ```js 9 | * import {expose, createProxyStore} from '...' 10 | * const store = createStore(reducer) 11 | * const proxyStore = createProxyStore(store) 12 | * expose(proxyStore, self) 13 | *``` 14 | * @param proxyStore A proxy store created using `createProxyStore` 15 | * @param context Typically `self` on a worker module 16 | */ 17 | export const expose = ( 18 | proxyStore: ProxyStore, 19 | context: Comlink.Endpoint | Window 20 | ): void => { 21 | Comlink.expose({ ...proxyStore }, context) 22 | } 23 | -------------------------------------------------------------------------------- /src/getProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useStore } from './hooks' 3 | import { StateContext } from './StateContext' 4 | import { StoreContext } from './StoreContext' 5 | 6 | interface ProviderProps { 7 | children: React.ReactNode 8 | fallback?: JSX.Element 9 | } 10 | 11 | const empty = <> 12 | 13 | export function getProvider(worker: Worker) { 14 | return function Provider({ children, fallback = empty }: ProviderProps) { 15 | const [state, store] = useStore(worker) 16 | 17 | const provider = ( 18 | 19 | {children} 20 | 21 | ) 22 | 23 | return state ? provider : fallback 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import * as Comlink from 'comlinkjs' 2 | import { useContext, useEffect, useState } from 'react' 3 | import { AnyAction, Dispatch } from 'redux' 4 | import { ProxyStore } from './ProxyStore' 5 | import { StoreContext } from './StoreContext' 6 | import { StateContext } from './StateContext' 7 | type Selector = (state: T) => any 8 | 9 | export function useStore(worker: Worker): [T | null, ProxyStore] { 10 | const [state, setState] = useState(null) 11 | const proxyStore: ProxyStore = Comlink.proxy(worker) as any 12 | 13 | // get current state then subscribe to it 14 | useEffect(() => { 15 | proxyStore.getState().then(async (s: T) => setState(s)) 16 | proxyStore.subscribe(Comlink.proxyValue((s: T) => setState(s))) 17 | }, []) // only on first render 18 | 19 | return [state, proxyStore] 20 | } 21 | 22 | export const useSelector = (selector: Selector): any => { 23 | const state = useContext(StateContext) 24 | return selector(state) 25 | } 26 | 27 | export const useDispatch = (): Dispatch => 28 | useContext(StoreContext).dispatch 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { useDispatch, useStore, useSelector } from './hooks' 2 | export { getProvider } from './getProvider' 3 | export { createProxyStore } from './createProxyStore' 4 | export { expose } from './expose' 5 | -------------------------------------------------------------------------------- /src/uniqueId.ts: -------------------------------------------------------------------------------- 1 | let lastId = 0 2 | export const uniqueId = () => ++lastId 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "baseUrl": "./", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "importHelpers": true, 8 | "jsx": "react", 9 | "lib": ["dom", "esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "paths": { 19 | "*": ["src/*", "node_modules/*"] 20 | }, 21 | "rootDir": "./", 22 | "sourceMap": true, 23 | "strict": true, 24 | "strictFunctionTypes": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "target": "es5" 28 | }, 29 | "include": ["src", "types"] 30 | } 31 | --------------------------------------------------------------------------------