50 |
51 | )
52 | }
53 |
54 | ReactDOM.render(, document.getElementById('root'))
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devhammed/use-global-hook",
3 | "version": "1.5.4",
4 | "description": "Painless global state management for React using Hooks and Context API in 1KB!",
5 | "main": "lib/index.js",
6 | "types": "index.d.ts",
7 | "private": false,
8 | "homepage": "https://devhammed.github.io/use-global-hook",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/devhammed/use-global-hook"
12 | },
13 | "scripts": {
14 | "clean": "rm -rf lib",
15 | "build": "rollup -c",
16 | "prepublish": "npm run clean && npm run build",
17 | "example": "parcel example/index.html",
18 | "predeploy": "rm -rf .cache && rm -rf dist && parcel build example/index.html --public-url /use-global-hook",
19 | "deploy": "gh-pages -d dist -m 'build: new release'"
20 | },
21 | "files": [
22 | "lib",
23 | "index.d.ts"
24 | ],
25 | "keywords": [
26 | "react",
27 | "state",
28 | "store",
29 | "hooks",
30 | "context-api"
31 | ],
32 | "author": "Hammed Oyedele (https://devhammed.github.io)",
33 | "license": "MIT",
34 | "devDependencies": {
35 | "@babel/cli": "^7.7.7",
36 | "@babel/core": "^7.7.7",
37 | "@babel/preset-env": "^7.7.7",
38 | "babel-core": "^6.26.3",
39 | "gh-pages": "^2.1.1",
40 | "is-obj": "^2.0.0",
41 | "parcel": "^1.12.4",
42 | "react": "^16.12.0",
43 | "react-dom": "^16.12.0",
44 | "rollup": "^1.27.14",
45 | "rollup-plugin-babel": "^4.3.2",
46 | "rollup-plugin-cleanup": "^3.1.1",
47 | "rollup-plugin-terser": "^5.1.3"
48 | },
49 | "peerDependencies": {
50 | "react": "^16.8.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const GlobalHooksContext = React.createContext()
4 |
5 | export function GlobalHooksProvider ({ hooks, children }) {
6 | let globalHooks = {}
7 | const parentContext = React.useContext(GlobalHooksContext)
8 |
9 | if ({}.toString.call(hooks) !== '[object Array]') {
10 | throw new TypeError(
11 | 'You must provide a hooks array to initialize .'
12 | )
13 | }
14 |
15 | // felt cute, might delete later...
16 | if (parentContext) {
17 | globalHooks = { ...parentContext }
18 | }
19 |
20 | hooks.forEach(hook => {
21 | if (typeof hook !== 'function') {
22 | throw new TypeError(
23 | `Provided global hook value "${hook}" is not a function.`
24 | )
25 | }
26 |
27 | const hookName = hook.globalHookName
28 |
29 | if (typeof hookName !== 'string') {
30 | throw new SyntaxError(
31 | 'One of your Global Hook functions did not initialize correctly, pass it to `createGlobalHook` function with the unique name to fix this error.'
32 | )
33 | }
34 |
35 | if (hookName in globalHooks) {
36 | throw new SyntaxError(
37 | `Don't duplicate entry in global hooks, a hook with the name ${hookName} already exist.`
38 | )
39 | }
40 |
41 | globalHooks[hookName] = hook()
42 | })
43 |
44 | return React.createElement(
45 | GlobalHooksContext.Provider,
46 | { value: globalHooks },
47 | children
48 | )
49 | }
50 |
51 | export function createGlobalHook (name, fn) {
52 | if (typeof fn !== 'function') {
53 | throw new TypeError(`Provided hook value for "${name}" is not a function.`)
54 | }
55 |
56 | const globalHookFunction = (...args) => fn(...args)
57 | globalHookFunction.globalHookName = name
58 |
59 | return globalHookFunction
60 | }
61 |
62 | export function useGlobalHook (name) {
63 | const context = React.useContext(GlobalHooksContext)
64 |
65 | if (!context) {
66 | throw new SyntaxError(
67 | 'You must wrap your components with a .'
68 | )
69 | }
70 |
71 | const value = context[name]
72 |
73 | if (!value) {
74 | throw new ReferenceError(
75 | `Provided store instance for "${name}" did not initialize correctly.`
76 | )
77 | }
78 |
79 | return value
80 | }
81 |
82 | export function withGlobalHooks (component, hooks) {
83 | if (!component) {
84 | throw new TypeError(
85 | 'You cannot pass in empty component to withGlobalHooks.'
86 | )
87 | }
88 |
89 | if ({}.toString.call(hooks) !== '[object Array]') {
90 | throw new TypeError(
91 | 'You must provide a hooks name array to initialize withGlobalHooks.'
92 | )
93 | }
94 |
95 | const withGlobalHOC = props => {
96 | const stores = {}
97 |
98 | hooks.forEach(hook => {
99 | stores[hook] = useGlobalHook(hook)
100 | })
101 |
102 | return React.createElement(component, { ...props, ...stores })
103 | }
104 |
105 | withGlobalHOC.displayName = `withGlobalHooks(${component.name})`
106 |
107 | return withGlobalHOC
108 | }
109 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * use-global-hook is built on top of React hooks, context and patterns surrounding those elements.
3 | *
4 | * It has four pieces:
5 | * - hooks
6 | * - createGlobalHook
7 | * - useGlobalHook
8 | * - ``
9 | */
10 |
11 | import React, { FC } from 'react'
12 |
13 | /**
14 | * A hook store is a place to store state and some of the logic for updating it.
15 | *
16 | * Store is a very simple React hook (which means you can re-use it, use other
17 | * hooks within it, use your existing custom or third-party hooks, etc).
18 | *
19 | * You should wrap the hook function in `createGlobalHook` function with the first parameter which is the unique identifier for the hook.
20 | *
21 | * Wrapping the function means, in case of when creating dynamic hook function, any argument you intend to pass to your hook when will be applied automatically, you still have your function the way you declare it --- cheers! e.g something like `counterStore(props.dynamicValue)` though this can only happen when registering the hook function in ``.
22 | *
23 | * ```tsx
24 | * import { useState } from 'react'
25 | * import { createGlobalHook } from '@devhammed/use-global-hook'
26 | *
27 | * const counterStore = createGlobalHook('counterStore', () => {
28 | * const [count, setCount] = useState(0)
29 | * const increment = () => setCount(count + 1)
30 | * const decrement = () => setCount(count - 1)
31 | *
32 | * return { count, increment, decrement }
33 | * })
34 | * ```
35 | *
36 | * Note that hooks prop use useState hook from React for managing state
37 | * When you call setState it triggers components to re-render, so be careful not
38 | * to mutate state directly or your components won't re-render.
39 | */
40 | export declare type StoreHook = () => any
41 |
42 | /**
43 | * Hook store function wrapper, this function apply some internally used property to a function that calls your original function. A wrapper function is best for this case as it is not a good idea to mutate your original function with properties that may conflict and third-party hooks is always taking into consideration where it is not good to add properties to the library core function and this method also allows creating clone of same hook function without conflicting names.
44 | */
45 | export declare function createGlobalHook (
46 | name: string,
47 | fn: (...args) => any
48 | ): (...args) => any
49 |
50 | /**
51 | * Collection of Store Hooks
52 | */
53 | export declare type StoreHooks = {
54 | [key: string]: StoreHook
55 | }
56 |
57 | /**
58 | * Global Store Hooks context container
59 | */
60 | export declare const GlobalHooksContext: React.Context
61 |
62 | export interface GlobalHooksProviderProps {
63 | /**
64 | * An object of hooks that will be available to retrieve with useGlobalHook
65 | */
66 | hooks: StoreHooks
67 |
68 | /**
69 | * An array of React element children
70 | */
71 | children: React.ReactChild[]
72 | }
73 |
74 | /**
75 | * The `` component has two roles:
76 | *
77 | * - It initializes global instances of given hooks array (an Array is required because React expects the number of hooks to be consistent across re-renders and Objects are not guaranteed to return in same order)
78 | * - It uses context to pass initialized instances of given hooks to all the components
79 | * down the tree
80 | *
81 | * ```tsx
82 | * ReactDOM.render(
83 | *
84 | *
85 | *
86 | *
87 | * )
88 | * ```
89 | */
90 | export declare function GlobalHooksProvider (
91 | props: GlobalHooksProviderProps
92 | ): FC
93 |
94 | /**
95 | * Next we'll need a piece to introduce our state back into the tree so that:
96 | *
97 | * - When state changes, our components re-render.
98 | * - We can depend on our store state.
99 | * - We can call functions exposed by the store.
100 | *
101 | * For this we have the `useGlobalHook` hook which allows us to get global store instances by using passing the value we used when creating the global hook with `createGlobalHook` function.
102 | *
103 | * ```tsx
104 | * function Counter() {
105 | * const { count, decrement, increment } = useGlobalHook('counterStore')
106 | *
107 | * return (
108 | *
109 | * {count}
110 | *
111 | *
112 | *
113 | * )
114 | * }
115 | * ```
116 | */
117 | export declare function useGlobalHook (key: string): StoreHook
118 |
119 | /**
120 | * Class components can benefit from hooks too!
121 | *
122 | * You heard that right, who says class component cannot use and benefit from the awesomeness of React hooks?
123 | use-global-hooks provides a function component HOC wrapper `withGlobalHooks` which allows class components to use hooks state(s) by passing them props. cool right? let's look at how above Counter component will look when using a class.
124 | * It is as easy as using function component too, just pass in your component variable as the first parameter and second parameter is an array that contains names of the global hooks you want to use and you will be able access the state from `this.props.[globalHookName]`.
125 | * The HOC wrapper pass down props so any other prop you are using in your component still works fine except if there is prop conflict which is why it is recommended you add `Store` suffix to your store names when creating them.
126 | * So with support for class component, you can start using this library even when you are not ready to switch to function components.
127 | * NOTE: You can also use `withGlobalHooks` with function components but why not just use the hook? :wink:
128 | *
129 | * ```tsx
130 | * import { withGlobalHooks } from '@devhammed/use-global-hook'
131 | *
132 | * class Counter extends React.Component {
133 | * render () {
134 | * const { count, increment, decrement, reset } = this.props.counterStore
135 | *
136 | * return (
137 | *
138 | *
139 | * {count}
140 | *
141 | *
142 | *
143 | * )
144 | * }
145 | * }
146 | *
147 | * export default withGlobalHooks(Counter, ['counterStore'])
148 | * ```
149 | */
150 | export declare function withGlobalHooks (
151 | component: React.ReactElement,
152 | hooks: string[]
153 | ): React.FunctionComponent
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # use-global-hook
2 |
3 | > Painless global state management for React using Hooks and Context API in 1KB!
4 |
5 | [](https://www.npmjs.com/package/@devhammed/use-global-hook) [](https://standardjs.com) [](https://github.com/acekyd/made-in-nigeria)
6 |
7 | ## Installation
8 |
9 | ```sh
10 | npm install @devhammed/use-global-hook
11 | ```
12 |
13 | ## Quick Example
14 |
15 | ```jsx
16 | import React from 'react'
17 | import ReactDOM from 'react-dom'
18 | import { GlobalHooksProvider, createGlobalHook, useGlobalHook } from '@devhammed/use-global-hook'
19 |
20 | const store = createGlobalHook(/** 1 **/ 'counterStore', () => {
21 | const [count, setCount] = React.useState(0)
22 |
23 | const increment = () => setCount(count + 1)
24 | const decrement = () => setCount(count - 1)
25 | const reset = () => setCount(0)
26 |
27 | return { count, increment, decrement, reset }
28 | })
29 |
30 | function Counter () {
31 | const { count, increment, decrement, reset } = useGlobalHook('counterStore') /** 1. This is where you use the name you defined in `createGlobalHook` function, this name should be unique through out your app **/
32 |
33 | return (
34 |
35 |
36 | {count}
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | function App () {
44 | return (
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | ReactDOM.render(, document.getElementById('root'))
54 | ```
55 |
56 | Notice how we render the `Counter` component three times? clicking on a button in one of the component instance will update others too.
57 |
58 | See it running live [here](https://devhammed.github.io/use-global-hook).
59 |
60 | ### Concepts
61 |
62 | use-global-hook is built on top of React Hooks and Context API. I first used this concept in a project at my workplace [Epower](https://epower.ng) and seeing the re-usability and convenience, I decided to convert it to a standalone open-source library for others to benefit from the awesomeness of React Hooks.
63 |
64 | ##### `Store`
65 |
66 | A hook store is a place to store state and some of the logic for updating it.
67 |
68 | Store is a very simple React hook wrapper (which means you can re-use it, use other hooks within it, etc).
69 |
70 | `createGlobalHook` is a Hook store function wrapper, this function is used to apply some internally used property to a function that calls your original hook function. A wrapper function is best for this case as it is not a good practice to mutate your original function with properties that may conflict and third-party hooks is taking into consideration where it is not good to add properties to the library core exports and this method also allows creating clone of same hook function without conflicting instances.
71 |
72 | Wrapping the function means, in case of when creating dynamic hook function, any argument you intend to pass to your hook when will be applied automatically, you still have your function the way you declare it and the way you intend to use it --- cheers! e.g something like `store(props.dynamicValue)` though this can only happen when registering the hook function in ``.
73 |
74 | ```js
75 | import React from 'React'
76 | import { createGlobalHook } from '@devhammed/use-global-hook'
77 |
78 | const store = createGlobalHook('counterStore', () => {
79 | const [count, setCount] = React.useState(0)
80 |
81 | const increment = () => setCount(count + 1)
82 | const decrement = () => setCount(count - 1)
83 | const reset = () => setCount(0)
84 |
85 | return { count, increment, decrement, reset }
86 | })
87 | ```
88 |
89 | This example uses the official React `useState()` hook, but you are not limited to this only, there are other hooks like `useReducer()` if you need something like Redux or any custom or third-party hook as far as it follows the Rules of Hooks, you can read more on Hooks on React official website [here](https://reactjs.org/docs/hooks-intro.html).
90 |
91 | ##### `useGlobalHook`
92 |
93 | Next we'll need a piece to introduce our state back into the tree so that:
94 |
95 | - When state changes, our components re-render.
96 | - We can depend on our store state.
97 | - We can call functions exposed by the store.
98 |
99 | For this we have the `useGlobalHook` hook which allows us to get global store instances by using passing the value we used when creating the global hook with `createGlobalHook` function.
100 |
101 | ```jsx
102 | function Counter () {
103 | const { count, decrement, increment } = useGlobalHook('counterStore')
104 |
105 | return (
106 |
189 | )
190 | }
191 |
192 | function App () {
193 | return (
194 |
195 |
196 |
197 |
198 |
199 |
200 | )
201 | }
202 |
203 | ReactDOM.render(, document.getElementById('root'))
204 | ```
205 |
206 | [View Demo](https://6fhi2.codesandbox.io)
207 |
208 | See how the `Time` components wrapped by `Timer` are able to access both `counterStore` and `timerStore`?
209 | When you render `GlobalHooksProvider`, one of the things it does under the hood is to try to use it parent global context, If it is undefined this means that it is the root else it merges with the parent context and you are able to access any store hook from parent(s) but the parent cannot access child nested global state because data flows in one direction else you define a function in global state that will communicate to other component.
210 |
211 | ### Class Components
212 | You heard that right, who says class component cannot use and benefit from the awesomeness of React hooks?
213 | use-global-hooks provides a function component HOC wrapper `withGlobalHooks` which allows class components to use hooks state(s) by passing them props. cool right? let's look at how above Counter component will look when using a class.
214 |
215 | ```js
216 | // Counter.js
217 |
218 | import { withGlobalHooks } from '@devhammed/use-global-hook'
219 |
220 | class Counter extends React.Component {
221 | render () {
222 | const { count, increment, decrement, reset } = this.props.counterStore
223 |
224 | return (
225 |
226 |
227 | {count}
228 |
229 |
230 |
231 | )
232 | }
233 | }
234 |
235 | export default withGlobalHooks(Counter, ['counterStore'])
236 | ```
237 |
238 | It is as easy as using function component too, just pass in your component variable as the first parameter and second parameter is an array that contains names of the global hooks you want to use and you will be able access the state from `this.props.[globalHookName]`. The HOC wrapper pass down props so any other prop you are using in your component still works fine except if there is prop conflict which is why it is recommended you add `Store` suffix to your store names when creating them.
239 |
240 | So with support for class component, you can start using this library even when you are not ready to switch to function components.
241 |
242 | NOTE: You can also use `withGlobalHooks` with function components but why not just use the hook? :wink:
243 |
244 | ### Testing
245 |
246 | Global Hooks are just your regular hooks too, so you can easily test with `react-hooks-testing-library` library e.g
247 |
248 | ```js
249 | import { renderHook, act } from 'react-hooks-testing-library'
250 |
251 | test('counter', async () => {
252 | let count, increment, decrement
253 | renderHook(() => ({count, increment, decrement} = counterStore()))
254 |
255 | expect(count).toBe(0)
256 |
257 | act(() => increment())
258 | expect(count).toBe(1)
259 |
260 | act(() => decrement())
261 | expect(count).toBe(0)
262 | })
263 | ```
264 |
265 | ### Pro-Tip
266 |
267 | Create a file like `storeNames.{js,ts}` that contains names of your stores so you can re-use the strings to avoid making mistake when typing or when refactoring so you will have to change the names in one place, see example below:
268 |
269 | ```js
270 | // utils/storeNames.js
271 |
272 | export const API_STORE = 'apiStore'
273 |
274 | export const COUNTER_STORE = 'counterStore'
275 | ```
276 |
277 | Then you can create your store like this...
278 |
279 | ```js
280 | // utils/mainStore.js
281 |
282 | import { COUNTER_STORE } from '../utils/storeNames.js'
283 |
284 | const counterStoreHook = createGlobalHook(COUNTER_STORE, () => {
285 | const [count, setCount] = React.useState(0)
286 |
287 | const increment = () => setCount(count + 1)
288 | const decrement = () => setCount(count - 1)
289 | const reset = () => setCount(0)
290 |
291 | return { count, increment, decrement, reset }
292 | })
293 |
294 | ```
295 |
296 | Then register in your root component and you use it anywhere like this:
297 |
298 | ```jsx
299 | // pages/counter.js
300 |
301 | import { COUNTER_STORE } from '../utils/storeNames.js'
302 |
303 | function Counter () {
304 | const { count, increment, decrement, reset } = useGlobalHook(COUNTER_STORE)
305 |
306 | return (
307 |