├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── funding.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── examples
├── counter
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── package.json
├── i18n
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── package.json
├── index.html
├── index.js
├── package.json
├── theming
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── package.json
├── tsconfig.json
├── typescript
│ ├── App.tsx
│ ├── index.html
│ ├── index.tsx
│ ├── package.json
│ └── tsconfig.json
├── wizard-form
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── package.json
└── yarn.lock
├── jest.config.js
├── logo
├── logo-black.png
├── logo-black.svg
├── logo-white.png
├── logo-white.svg
├── logo.png
└── logo.svg
├── package.json
├── rollup.config.js
├── src
└── index.tsx
├── test
└── index.test.tsx
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage
2 | dist
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "babel-eslint",
3 | extends: ["airbnb", "prettier/react", "plugin:prettier/recommended"],
4 | env: {
5 | jest: true,
6 | browser: true,
7 | },
8 | settings: {
9 | "import/resolver": {
10 | node: {
11 | extensions: [".js", ".jsx", ".ts", ".tsx"],
12 | },
13 | },
14 | },
15 | rules: {
16 | camelcase: "off",
17 | "no-use-before-define": "off",
18 | "no-underscore-dangle": "off",
19 | "no-bitwise": "off",
20 | "no-restricted-syntax": "off",
21 | "react/jsx-props-no-spreading": "off",
22 | "react/no-array-index-key": "off",
23 | "react/prop-types": "off",
24 | "react/button-has-type": "off",
25 | "react/forbid-prop-types": "off",
26 | "react/jsx-filename-extension": "off",
27 | "react/require-default-props": "off",
28 | "react/destructuring-assignment": "off",
29 | "import/no-extraneous-dependencies": "off",
30 | "import/prefer-default-export": "off",
31 | "import/extensions": "off",
32 | "jsx-a11y/no-autofocus": "off",
33 | "jsx-a11y/anchor-is-valid": "off",
34 | "jsx-a11y/label-has-for": "off",
35 | "jsx-a11y/label-has-associated-control": "off",
36 | },
37 | overrides: [
38 | {
39 | files: ["**/*.ts", "**/*.tsx"],
40 | parser: "@typescript-eslint/parser",
41 | plugins: ["@typescript-eslint"],
42 | rules: {
43 | "no-undef": "off",
44 | "no-unused-vars": "off",
45 | "no-restricted-globals": "off",
46 | "no-use-before-define": "off",
47 | },
48 | },
49 | ],
50 | };
51 |
--------------------------------------------------------------------------------
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | github: diegohaz
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-20.04
8 |
9 | strategy:
10 | matrix:
11 | node-version: [10.x]
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 |
20 | - name: Install dependencies
21 | run: yarn install --frozen-lockfile
22 |
23 | - run: yarn lint
24 | - run: yarn test --coverage
25 | - run: yarn type-check
26 | - run: yarn build
27 |
28 | - name: Upload coverage to Codecov
29 | uses: codecov/codecov-action@v2
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | coverage
4 | dist
5 | *.log
6 | .cache
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact = true
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 12
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "editor.formatOnSave": false,
4 | "eslint.enable": true,
5 | "editor.codeActionsOnSave": {
6 | "source.fixAll.eslint": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [3.3.3](https://github.com/diegohaz/constate/compare/v3.3.2...v3.3.3) (2025-04-01)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * Allow React 19 as a peer dependency ([#161](https://github.com/diegohaz/constate/issues/161)) ([2054d86](https://github.com/diegohaz/constate/commit/2054d86b918a93c6be97bcf4f0705b3eb6326c25))
11 |
12 | ### [3.3.2](https://github.com/diegohaz/constate/compare/v3.3.1...v3.3.2) (2022-04-18)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * Allow React 18 as a peer dependency ([6611f1c](https://github.com/diegohaz/constate/commit/6611f1c74f12281d6e7e7d0c2633f9d7178d2efb))
18 |
19 | ### [3.3.1](https://github.com/diegohaz/constate/compare/v3.3.0...v3.3.1) (2022-04-18)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * Fix `React.FC` types for React 18 ([#155](https://github.com/diegohaz/constate/issues/155)) ([77de7c7](https://github.com/diegohaz/constate/commit/77de7c7c666545f92e2c4338096e39980dbadcff))
25 |
26 | ## [3.3.0](https://github.com/diegohaz/constate/compare/v3.2.0...v3.3.0) (2021-06-24)
27 |
28 |
29 | ### Features
30 |
31 | * Add debug message for no provider case ([#138](https://github.com/diegohaz/constate/issues/138)) ([dcdf1fb](https://github.com/diegohaz/constate/commit/dcdf1fb62660a530f04eea5bfff3b6f3a9a45d84))
32 |
33 | ## [3.2.0](https://github.com/diegohaz/constate/compare/v3.1.0...v3.2.0) (2021-03-01)
34 |
35 |
36 | ### Features
37 |
38 | * Show displayName for each context in React Developer Tools ([#128](https://github.com/diegohaz/constate/issues/128)) ([6c1d5e0](https://github.com/diegohaz/constate/commit/6c1d5e0f22d07c57e85c6784b00e27958596e51b))
39 |
40 | ## [3.1.0](https://github.com/diegohaz/constate/compare/v3.0.1...v3.1.0) (2020-10-22)
41 |
42 |
43 | ### Features
44 |
45 | * Allow React 17 as a peerDependency ([#122](https://github.com/diegohaz/constate/issues/122)) ([0f69b83](https://github.com/diegohaz/constate/commit/0f69b8336a53a6ef8797795bc618dc3b884a189c))
46 |
47 | ### [3.0.1](https://github.com/diegohaz/constate/compare/v3.0.0...v3.0.1) (2020-09-08)
48 |
49 |
50 | ### Bug Fixes
51 |
52 | * Use empty object instead of string for NO_PROVIDER value ([28dd2f8](https://github.com/diegohaz/constate/commit/28dd2f8c8c59e17bd0f025f7e5df08bf381c77c9))
53 |
54 | ## [3.0.0](https://github.com/diegohaz/constate/compare/v2.0.0...v3.0.0) (2020-09-08)
55 |
56 |
57 | ### ⚠ BREAKING CHANGES
58 |
59 | * Types now depend on TypeScript v4.0.
60 | * The deprecated function/object API has been removed.
61 |
62 | **Before**:
63 | ```jsx
64 | import createUseContext from "constate";
65 | const useCounterContext = createUseContext(useCounter);
66 |
67 | ...
68 |
69 | ```
70 |
71 | **After**:
72 | ```jsx
73 | import constate from "constate";
74 | const [CounterProvider, useCounterContext] = constate(useCounter);
75 |
76 | ...
77 |
78 | ```
79 |
80 | ### Features
81 |
82 | * Upgrade to TypeScript 4 and remove deprecated API ([#118](https://github.com/diegohaz/constate/issues/118)) ([19e6b6a](https://github.com/diegohaz/constate/commit/19e6b6a0645b61e6163be5ad80a789cb3aa2a40d)), closes [#109](https://github.com/diegohaz/constate/issues/109) [#117](https://github.com/diegohaz/constate/issues/117)
83 |
84 | ## [2.0.0](https://github.com/diegohaz/constate/compare/v1.3.2...v2.0.0) (2020-02-15)
85 |
86 |
87 | ### ⚠ BREAKING CHANGES
88 |
89 | * Support for the `createMemoDeps` parameter has been dropped.
90 |
91 | **Before**:
92 | ```jsx
93 | const useCounterContext = createUseContext(useCounter, value => [value.count]);
94 | ```
95 |
96 | **After**:
97 | ```jsx
98 | const useCounterContext = createUseContext(() => {
99 | const value = useCounter();
100 | return useMemo(() => value, [value.count]);
101 | });
102 | ```
103 |
104 | ### Features
105 |
106 | * Deprecate old function/object API ([#101](https://github.com/diegohaz/constate/issues/101)) ([c102a31](https://github.com/diegohaz/constate/commit/c102a31d00b23026256f09bbede11488e04f6dc2))
107 | * Remove deprecated `createMemoDeps` parameter ([#100](https://github.com/diegohaz/constate/issues/100)) ([553405d](https://github.com/diegohaz/constate/commit/553405df07144cde2009a9e2590012cd1fee548f))
108 |
109 | ### [1.3.2](https://github.com/diegohaz/constate/compare/v1.3.1...v1.3.2) (2019-10-20)
110 |
111 |
112 | ### Bug Fixes
113 |
114 | * Remove unnecessary code from production ([a0d22bf](https://github.com/diegohaz/constate/commit/a0d22bfebc409dbea081c13ac61a84ca2022bc0b))
115 |
116 | ### [1.3.1](https://github.com/diegohaz/constate/compare/v1.3.0...v1.3.1) (2019-10-20)
117 |
118 |
119 | ### Bug Fixes
120 |
121 | * Fix invalid attempt to destructure non-iterable instance ([67001c4](https://github.com/diegohaz/constate/commit/67001c46f981ce378538136b0dcb9f9864d54c10))
122 |
123 | ## [1.3.0](https://github.com/diegohaz/constate/compare/v1.2.0...v1.3.0) (2019-10-20)
124 |
125 |
126 | ### Features
127 |
128 | * Add `sideEffects` field to `package.json` ([97c0af5](https://github.com/diegohaz/constate/commit/97c0af5b60d102471ec91690172c3bb57d386c2c))
129 | * Expose API for splitting custom hook into multiple contexts ([#97](https://github.com/diegohaz/constate/issues/97)) ([fc3426e](https://github.com/diegohaz/constate/commit/fc3426ed2a9e5f7c05b2a069efb00c6b4d4e76cd)), closes [#93](https://github.com/diegohaz/constate/issues/93)
130 |
131 |
132 |
133 | ## [1.2.0](https://github.com/diegohaz/constate/compare/v1.1.1...v1.2.0) (2019-06-29)
134 |
135 |
136 | ### Features
137 |
138 | * Set displayName of Context and Provider ([#79](https://github.com/diegohaz/constate/issues/79)) ([#80](https://github.com/diegohaz/constate/issues/80)) ([fc6595f](https://github.com/diegohaz/constate/commit/fc6595f))
139 |
140 |
141 |
142 | ## [1.1.1](https://github.com/diegohaz/constate/compare/v1.1.0...v1.1.1) (2019-04-14)
143 |
144 |
145 | ### Bug Fixes
146 |
147 | * Fix React peer dependency range ([7132e3d](https://github.com/diegohaz/constate/commit/7132e3d))
148 |
149 |
150 |
151 | # [1.1.0](https://github.com/diegohaz/constate/compare/v1.0.0...v1.1.0) (2019-04-14)
152 |
153 |
154 | ### Features
155 |
156 | * Return a hook from the createContainer method ([#78](https://github.com/diegohaz/constate/issues/78)) ([8de6eb6](https://github.com/diegohaz/constate/commit/8de6eb6)), closes [#77](https://github.com/diegohaz/constate/issues/77)
157 |
158 |
159 |
160 | # [1.0.0](https://github.com/diegohaz/constate/compare/v0.9.0...v1.0.0) (2019-02-06)
161 |
162 | First official release.
163 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Diego Haz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Constate
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Write local state using [React Hooks](https://reactjs.org/docs/hooks-intro.html) and lift it up to [React Context](https://reactjs.org/docs/context.html) only when needed with minimum effort.
15 |
16 |
17 |
18 |
34 |
35 |
36 |
37 | ## Basic example
38 |
39 | ```jsx
40 | import React, { useState } from "react";
41 | import constate from "constate";
42 |
43 | // 1️⃣ Create a custom hook as usual
44 | function useCounter() {
45 | const [count, setCount] = useState(0);
46 | const increment = () => setCount(prevCount => prevCount + 1);
47 | return { count, increment };
48 | }
49 |
50 | // 2️⃣ Wrap your hook with the constate factory
51 | const [CounterProvider, useCounterContext] = constate(useCounter);
52 |
53 | function Button() {
54 | // 3️⃣ Use context instead of custom hook
55 | const { increment } = useCounterContext();
56 | return ;
57 | }
58 |
59 | function Count() {
60 | // 4️⃣ Use context in other components
61 | const { count } = useCounterContext();
62 | return {count};
63 | }
64 |
65 | function App() {
66 | // 5️⃣ Wrap your components with Provider
67 | return (
68 |
69 |
70 |
71 |
72 | );
73 | }
74 | ```
75 |
76 | [Learn more](#api)
77 |
78 | ## Advanced example
79 |
80 | ```jsx
81 | import React, { useState, useCallback } from "react";
82 | import constate from "constate";
83 |
84 | // 1️⃣ Create a custom hook that receives props
85 | function useCounter({ initialCount = 0 }) {
86 | const [count, setCount] = useState(initialCount);
87 | // 2️⃣ Wrap your updaters with useCallback or use dispatch from useReducer
88 | const increment = useCallback(() => setCount(prev => prev + 1), []);
89 | return { count, increment };
90 | }
91 |
92 | // 3️⃣ Wrap your hook with the constate factory splitting the values
93 | const [CounterProvider, useCount, useIncrement] = constate(
94 | useCounter,
95 | value => value.count, // becomes useCount
96 | value => value.increment // becomes useIncrement
97 | );
98 |
99 | function Button() {
100 | // 4️⃣ Use the updater context that will never trigger a re-render
101 | const increment = useIncrement();
102 | return ;
103 | }
104 |
105 | function Count() {
106 | // 5️⃣ Use the state context in other components
107 | const count = useCount();
108 | return {count};
109 | }
110 |
111 | function App() {
112 | // 6️⃣ Wrap your components with Provider passing props to your hook
113 | return (
114 |
115 |
116 |
117 |
118 | );
119 | }
120 | ```
121 |
122 | [Learn more](#splitvalues)
123 |
124 | ## Installation
125 |
126 | npm:
127 |
128 | ```sh
129 | npm i constate
130 | ```
131 |
132 | Yarn:
133 |
134 | ```sh
135 | yarn add constate
136 | ```
137 |
138 | ## API
139 |
140 | ### `constate(useValue[, ...selectors])`
141 |
142 | Constate exports a single factory method. As parameters, it receives [`useValue`](#usevalue) and optional [`selector`](#selectors) functions. It returns a tuple of `[Provider, ...hooks]`.
143 |
144 | #### `useValue`
145 |
146 | It's any [custom hook](https://reactjs.org/docs/hooks-custom.html):
147 |
148 | ```js
149 | import { useState } from "react";
150 | import constate from "constate";
151 |
152 | const [CountProvider, useCountContext] = constate(() => {
153 | const [count] = useState(0);
154 | return count;
155 | });
156 | ```
157 |
158 | You can receive props in the custom hook function. They will be populated with ``:
159 |
160 | ```jsx
161 | const [CountProvider, useCountContext] = constate(({ initialCount = 0 }) => {
162 | const [count] = useState(initialCount);
163 | return count;
164 | });
165 |
166 | function App() {
167 | return (
168 |
169 | ...
170 |
171 | );
172 | }
173 | ```
174 |
175 | The API of the containerized hook returns the same value(s) as the original, as long as it is a descendant of the Provider:
176 |
177 | ```jsx
178 | function Count() {
179 | const count = useCountContext();
180 | console.log(count); // 10
181 | }
182 | ```
183 |
184 | #### `selectors`
185 |
186 | Optionally, you can pass in one or more functions to split the custom hook value into multiple React Contexts. This is useful so you can avoid unnecessary re-renders on components that only depend on a part of the state.
187 |
188 | A `selector` function receives the value returned by [`useValue`](#usevalue) and returns the value that will be held by that particular Context.
189 |
190 | ```jsx
191 | import React, { useState, useCallback } from "react";
192 | import constate from "constate";
193 |
194 | function useCounter() {
195 | const [count, setCount] = useState(0);
196 | // increment's reference identity will never change
197 | const increment = useCallback(() => setCount(prev => prev + 1), []);
198 | return { count, increment };
199 | }
200 |
201 | const [Provider, useCount, useIncrement] = constate(
202 | useCounter,
203 | value => value.count, // becomes useCount
204 | value => value.increment // becomes useIncrement
205 | );
206 |
207 | function Button() {
208 | // since increment never changes, this will never trigger a re-render
209 | const increment = useIncrement();
210 | return ;
211 | }
212 |
213 | function Count() {
214 | const count = useCount();
215 | return {count};
216 | }
217 | ```
218 |
219 | ## Contributing
220 |
221 | If you find a bug, please [create an issue](https://github.com/diegohaz/constate/issues/new) providing instructions to reproduce it. It's always very appreciable if you find the time to fix it. In this case, please [submit a PR](https://github.com/diegohaz/constate/pulls).
222 |
223 | If you're a beginner, it'll be a pleasure to help you contribute. You can start by reading [the beginner's guide to contributing to a GitHub project](https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/).
224 |
225 | When working on this codebase, please use `yarn`. Run `yarn examples` to run examples.
226 |
227 | ## License
228 |
229 | MIT © [Diego Haz](https://github.com/diegohaz)
230 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | "@babel/preset-env",
5 | {
6 | modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
7 | loose: true,
8 | targets: {
9 | browsers: "defaults",
10 | },
11 | },
12 | ],
13 | "@babel/preset-typescript",
14 | "@babel/preset-react",
15 | ],
16 | plugins: ["@babel/plugin-proposal-object-rest-spread"],
17 | };
18 |
--------------------------------------------------------------------------------
/examples/counter/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import constate from "constate";
3 |
4 | // 1️⃣ Create a custom hook as usual
5 | function useCounter() {
6 | const [count, setCount] = useState(0);
7 | const increment = () => setCount((prevCount) => prevCount + 1);
8 | return { count, increment };
9 | }
10 |
11 | // 2️⃣ Wrap your hook with the constate factory
12 | const [CounterProvider, useCounterContext] = constate(useCounter);
13 |
14 | function Button() {
15 | // 3️⃣ Use context instead of custom hook
16 | const { increment } = useCounterContext();
17 | return ;
18 | }
19 |
20 | function Count() {
21 | // 4️⃣ Use context in other components
22 | const { count } = useCounterContext();
23 | return {count};
24 | }
25 |
26 | function App() {
27 | // 5️⃣ Wrap your components with Provider
28 | return (
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/examples/counter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Constate example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/counter/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render(, document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/examples/counter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constate-counter",
3 | "description": "A simple counter example using Constate",
4 | "version": "1.0.0",
5 | "private": true,
6 | "keywords": [
7 | "constate",
8 | "context",
9 | "state",
10 | "react",
11 | "hooks"
12 | ],
13 | "scripts": {
14 | "start": "parcel index.html --open",
15 | "build": "parcel build index.html"
16 | },
17 | "dependencies": {
18 | "constate": "3.2.0",
19 | "parcel-bundler": "1.12.4",
20 | "react": "17.0.1",
21 | "react-dom": "17.0.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/i18n/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import constate from "constate";
3 |
4 | const translations = {
5 | en: {
6 | selectLabel: "Select language",
7 | },
8 | pt: {
9 | selectLabel: "Selecione o idioma",
10 | },
11 | };
12 |
13 | function useI18n() {
14 | const [lang, setLang] = useState("en");
15 | const locales = Object.keys(translations);
16 | return { lang, locales, setLang };
17 | }
18 |
19 | const [I18NProvider, useI18NContext] = constate(useI18n);
20 |
21 | function useTranslation(key) {
22 | const { lang } = useI18NContext();
23 | return translations[lang][key];
24 | }
25 |
26 | function Select(props) {
27 | const { lang, locales, setLang } = useI18NContext();
28 | return (
29 |
34 | );
35 | }
36 |
37 | function Label(props) {
38 | const label = useTranslation("selectLabel");
39 | return ;
40 | }
41 |
42 | function App() {
43 | return (
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/examples/i18n/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Constate example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/i18n/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render(, document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/examples/i18n/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constate-i18n",
3 | "description": "A simple i18n example using Constate",
4 | "version": "1.0.0",
5 | "private": true,
6 | "keywords": [
7 | "constate",
8 | "context",
9 | "state",
10 | "react",
11 | "hooks",
12 | "i18n"
13 | ],
14 | "scripts": {
15 | "start": "parcel index.html --open",
16 | "build": "parcel build index.html"
17 | },
18 | "dependencies": {
19 | "constate": "3.2.0",
20 | "parcel-bundler": "1.12.4",
21 | "react": "17.0.1",
22 | "react-dom": "17.0.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Constate examples
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Router, Link } from "@reach/router";
4 | import Counter from "./counter/App";
5 | import I18n from "./i18n/App";
6 | import Theming from "./theming/App";
7 | import TypeScript from "./typescript/App";
8 | import WizardForm from "./wizard-form/App";
9 |
10 | const paths = {
11 | counter: Counter,
12 | i18n: I18n,
13 | theming: Theming,
14 | typescript: TypeScript,
15 | "wizard-form": WizardForm,
16 | };
17 |
18 | const App = () => (
19 |
20 |
21 | {Object.keys(paths).map((path) => (
22 | -
23 | {path}
24 |
25 | ))}
26 |
27 |
28 |
29 | {Object.entries(paths).map(([path, Component]) => (
30 |
31 | ))}
32 |
33 |
34 |
35 | );
36 |
37 | ReactDOM.render(, document.getElementById("root"));
38 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": [
4 | "*"
5 | ],
6 | "scripts": {
7 | "start": "parcel index.html --open --no-hmr",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "@reach/router": "1.3.4",
12 | "parcel-bundler": "1.12.4",
13 | "react": "17.0.1",
14 | "react-dom": "17.0.1"
15 | },
16 | "alias": {
17 | "constate": "../src",
18 | "react": "./node_modules/react",
19 | "react-dom": "./node_modules/react-dom"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/theming/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { GithubPicker } from "react-color";
3 | import constate from "constate";
4 |
5 | const [ThemeProvider, useThemeContext, useThemeColor] = constate(
6 | (props) => useState(props.initialColor),
7 | (value) => value,
8 | ([color]) => color
9 | );
10 | const [PickerVisibilityProvider, usePickerVisibilityContext] = constate(() =>
11 | useState(false)
12 | );
13 |
14 | function Picker() {
15 | const [color, setColor] = useThemeContext();
16 | const [visible, setVisible] = usePickerVisibilityContext();
17 | return visible ? (
18 | {
23 | setColor(c.hex);
24 | setVisible(false);
25 | }}
26 | />
27 | ) : null;
28 | }
29 |
30 | function Button() {
31 | const background = useThemeColor();
32 | const [visible, setVisible] = usePickerVisibilityContext();
33 | return (
34 |
37 | );
38 | }
39 |
40 | function App() {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/examples/theming/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Constate example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/theming/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render(, document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/examples/theming/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constate-theming",
3 | "description": "A simple theming example using Constate",
4 | "version": "1.0.0",
5 | "private": true,
6 | "keywords": [
7 | "constate",
8 | "context",
9 | "state",
10 | "react",
11 | "hooks",
12 | "theming"
13 | ],
14 | "scripts": {
15 | "start": "parcel index.html --open",
16 | "build": "parcel build index.html"
17 | },
18 | "dependencies": {
19 | "constate": "3.2.0",
20 | "parcel-bundler": "1.12.4",
21 | "react": "17.0.1",
22 | "react-color": "2.19.3",
23 | "react-dom": "17.0.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "constate": ["../src"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/typescript/App.tsx:
--------------------------------------------------------------------------------
1 | // It just works! No need to type anything explicitly.
2 | import * as React from "react";
3 | import constate from "constate";
4 |
5 | function useCounter({ initialCount = 0 } = {}) {
6 | const [count, setCount] = React.useState(initialCount);
7 | const increment = React.useCallback(() => setCount((c) => c + 1), []);
8 | return { count, increment };
9 | }
10 |
11 | const [CounterProvider, useCount, useIncrement] = constate(
12 | useCounter,
13 | (value) => value.count,
14 | (value) => value.increment
15 | );
16 |
17 | function IncrementButton() {
18 | const increment = useIncrement();
19 | return ;
20 | }
21 |
22 | function Count() {
23 | const count = useCount();
24 | return {count};
25 | }
26 |
27 | function App() {
28 | return (
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/examples/typescript/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Constate example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/typescript/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render(, document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/examples/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constate-typescript",
3 | "description": "A simple TypeScript example using Constate",
4 | "version": "1.0.0",
5 | "private": true,
6 | "keywords": [
7 | "constate",
8 | "context",
9 | "state",
10 | "react",
11 | "hooks"
12 | ],
13 | "scripts": {
14 | "start": "parcel index.html --open",
15 | "build": "parcel build index.html"
16 | },
17 | "dependencies": {
18 | "@types/react": "16.9.53",
19 | "@types/react-dom": "16.9.8",
20 | "constate": "3.2.0",
21 | "parcel-bundler": "1.12.4",
22 | "react": "17.0.1",
23 | "react-dom": "17.0.1",
24 | "typescript": "4.2.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "downlevelIteration": true,
5 | "target": "esnext",
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "jsx": "react",
9 | "strict": true,
10 | "noImplicitAny": true,
11 | "noImplicitThis": true,
12 | "strictFunctionTypes": false,
13 | "declaration": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "noImplicitReturns": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "stripInternal": true,
19 | "resolveJsonModule": true,
20 | "esModuleInterop": true,
21 | "removeComments": true
22 | }
23 | }
--------------------------------------------------------------------------------
/examples/wizard-form/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import constate from "constate";
3 |
4 | const [StepProvider, useStepContext] = constate(useStep);
5 | const [FormProvider, useFormContext, useFormValues] = constate(
6 | useFormState,
7 | (value) => value,
8 | (value) => value.values
9 | );
10 |
11 | function useStep({ initialStep = 0 } = {}) {
12 | const [step, setStep] = useState(initialStep);
13 | const next = () => setStep(step + 1);
14 | const previous = () => setStep(step - 1);
15 | return { step, next, previous };
16 | }
17 |
18 | function useFormState({ initialValues = {} } = {}) {
19 | const [values, setValues] = useState(initialValues);
20 | return {
21 | values,
22 | register: (name, initialValue) =>
23 | setValues((prevValues) => ({
24 | ...prevValues,
25 | [name]: prevValues[name] || initialValue,
26 | })),
27 | update: (name, value) =>
28 | setValues((prevValues) => ({ ...prevValues, [name]: value })),
29 | };
30 | }
31 |
32 | function useFormInput({ register, values, update, name, initialValue = "" }) {
33 | useEffect(() => register(name, initialValue), []);
34 | return {
35 | name,
36 | onChange: (e) => update(name, e.target.value),
37 | value: values[name] || initialValue,
38 | };
39 | }
40 |
41 | function AgeForm({ onSubmit }) {
42 | const state = useFormContext();
43 | const age = useFormInput({ name: "age", ...state });
44 | return (
45 |
54 | );
55 | }
56 |
57 | function NameEmailForm({ onSubmit, onBack }) {
58 | const state = useFormContext();
59 | const name = useFormInput({ name: "name", ...state });
60 | const email = useFormInput({ name: "email", ...state });
61 | return (
62 |
75 | );
76 | }
77 |
78 | function Values() {
79 | const values = useFormValues();
80 | return {JSON.stringify(values, null, 2)}
;
81 | }
82 |
83 | function Wizard() {
84 | const { step, next, previous } = useStepContext();
85 | const steps = [AgeForm, NameEmailForm];
86 | const isLastStep = step === steps.length - 1;
87 | const props = {
88 | onSubmit: isLastStep
89 | ? (values) => alert(JSON.stringify(values, null, 2)) // eslint-disable-line no-alert
90 | : next,
91 | onBack: previous,
92 | };
93 | return React.createElement(steps[step], props);
94 | }
95 |
96 | function App() {
97 | return (
98 |
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
107 | export default App;
108 |
--------------------------------------------------------------------------------
/examples/wizard-form/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Constate example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/wizard-form/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render(, document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/examples/wizard-form/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constate-wizard-form",
3 | "description": "A wizard form example using Constate",
4 | "version": "1.0.0",
5 | "private": true,
6 | "keywords": [
7 | "constate",
8 | "context",
9 | "state",
10 | "react",
11 | "hooks"
12 | ],
13 | "scripts": {
14 | "start": "parcel index.html --open",
15 | "build": "parcel build index.html"
16 | },
17 | "dependencies": {
18 | "constate": "3.2.0",
19 | "parcel-bundler": "1.12.4",
20 | "react": "17.0.1",
21 | "react-dom": "17.0.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | coveragePathIgnorePatterns: ["/node_modules/", "/test/"],
3 | setupFilesAfterEnv: ["raf/polyfill"],
4 | };
5 |
--------------------------------------------------------------------------------
/logo/logo-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diegohaz/constate/a11761ede253458a6f3041dd04116a6d97f5eda0/logo/logo-black.png
--------------------------------------------------------------------------------
/logo/logo-black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/logo/logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diegohaz/constate/a11761ede253458a6f3041dd04116a6d97f5eda0/logo/logo-white.png
--------------------------------------------------------------------------------
/logo/logo-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diegohaz/constate/a11761ede253458a6f3041dd04116a6d97f5eda0/logo/logo.png
--------------------------------------------------------------------------------
/logo/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constate",
3 | "version": "3.3.3",
4 | "description": "Yet another React state management library that lets you work with local state and scale up to global state with ease",
5 | "license": "MIT",
6 | "repository": "diegohaz/constate",
7 | "main": "dist/constate.cjs.js",
8 | "module": "dist/constate.es.js",
9 | "jsnext:main": "dist/constate.es.js",
10 | "unpkg": "dist/constate.min.js",
11 | "types": "dist/ts/src",
12 | "sideEffects": false,
13 | "author": {
14 | "name": "Diego Haz",
15 | "email": "hazdiego@gmail.com",
16 | "url": "https://github.com/diegohaz"
17 | },
18 | "files": [
19 | "dist",
20 | "src"
21 | ],
22 | "scripts": {
23 | "test": "jest",
24 | "test:all": "yarn lint && yarn build && yarn type-check && yarn test && yarn examples:build && yarn clean",
25 | "coverage": "npm test -- --coverage",
26 | "postcoverage": "open-cli coverage/lcov-report/index.html",
27 | "type-check": "tsc --noEmit",
28 | "lint": "eslint . --ext js,ts,tsx",
29 | "clean": "rimraf dist",
30 | "prebuild": "npm run clean",
31 | "build": "tsc --emitDeclarationOnly && rollup -c",
32 | "prerelease": "npm run lint && npm test && npm run build",
33 | "release": "standard-version",
34 | "postpublish": "git push origin HEAD --follow-tags",
35 | "prepare": "npm run examples:install",
36 | "examples": "npm run start --prefix examples",
37 | "examples:install": "yarn --cwd examples",
38 | "examples:build": "npm run build --prefix examples",
39 | "examples:upgrade": "yarn upgrade-interactive --latest --cwd examples"
40 | },
41 | "husky": {
42 | "hooks": {
43 | "pre-commit": "lint-staged"
44 | }
45 | },
46 | "lint-staged": {
47 | "*.{js,ts,tsx}": [
48 | "eslint --ext js,ts,tsx --fix"
49 | ]
50 | },
51 | "keywords": [
52 | "constate"
53 | ],
54 | "devDependencies": {
55 | "@babel/cli": "7.13.0",
56 | "@babel/core": "7.13.8",
57 | "@babel/preset-env": "7.13.8",
58 | "@babel/preset-react": "7.12.13",
59 | "@babel/preset-typescript": "7.13.0",
60 | "@rollup/plugin-babel": "5.3.0",
61 | "@rollup/plugin-commonjs": "15.1.0",
62 | "@rollup/plugin-node-resolve": "9.0.0",
63 | "@rollup/plugin-replace": "2.4.1",
64 | "@testing-library/react": "11.2.5",
65 | "@types/jest": "26.0.20",
66 | "@types/prop-types": "15.7.3",
67 | "@types/react": "16.9.53",
68 | "@types/react-dom": "16.9.8",
69 | "@typescript-eslint/eslint-plugin": "4.15.2",
70 | "@typescript-eslint/parser": "4.15.2",
71 | "babel-eslint": "10.1.0",
72 | "babel-jest": "26.6.3",
73 | "eslint": "7.21.0",
74 | "eslint-config-airbnb": "18.2.1",
75 | "eslint-config-prettier": "6.14.0",
76 | "eslint-plugin-import": "2.22.1",
77 | "eslint-plugin-jsx-a11y": "6.4.1",
78 | "eslint-plugin-prettier": "3.3.1",
79 | "eslint-plugin-react": "7.22.0",
80 | "husky": "4.3.0",
81 | "jest": "26.6.3",
82 | "lint-staged": "10.5.4",
83 | "open-cli": "6.0.1",
84 | "prettier": "2.2.1",
85 | "raf": "3.4.1",
86 | "react": "17.0.1",
87 | "react-dom": "17.0.1",
88 | "react-test-renderer": "17.0.1",
89 | "rimraf": "3.0.2",
90 | "rollup": "2.40.0",
91 | "rollup-plugin-ignore": "1.0.9",
92 | "rollup-plugin-terser": "7.0.2",
93 | "standard-version": "9.1.1",
94 | "typescript": "4.2.2"
95 | },
96 | "peerDependencies": {
97 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { babel } from "@rollup/plugin-babel";
2 | import { nodeResolve } from "@rollup/plugin-node-resolve";
3 | import replace from "@rollup/plugin-replace";
4 | import commonjs from "@rollup/plugin-commonjs";
5 | import { terser } from "rollup-plugin-terser";
6 | import ignore from "rollup-plugin-ignore";
7 | import pkg from "./package.json";
8 |
9 | const external = Object.keys(pkg.peerDependencies || {});
10 | const allExternal = [...external, ...Object.keys(pkg.dependencies || {})];
11 | const extensions = [".ts", ".tsx", ".js", ".jsx", ".json"];
12 |
13 | const createCommonPlugins = () => [
14 | babel({
15 | extensions,
16 | babelHelpers: "bundled",
17 | exclude: "node_modules/**",
18 | }),
19 | ];
20 |
21 | const main = {
22 | input: "src/index.tsx",
23 | output: [
24 | {
25 | file: pkg.main,
26 | format: "cjs",
27 | exports: "named",
28 | },
29 | {
30 | file: pkg.module,
31 | format: "es",
32 | },
33 | ],
34 | external: allExternal,
35 | plugins: [...createCommonPlugins(), nodeResolve({ extensions })],
36 | };
37 |
38 | const unpkg = {
39 | input: "src/index.tsx",
40 | output: {
41 | name: pkg.name,
42 | file: pkg.unpkg,
43 | format: "umd",
44 | exports: "named",
45 | globals: {
46 | react: "React",
47 | },
48 | },
49 | external,
50 | plugins: [
51 | ...createCommonPlugins(),
52 | ignore(["stream"]),
53 | terser(),
54 | commonjs({
55 | include: /node_modules/,
56 | }),
57 | replace({
58 | "process.env.NODE_ENV": JSON.stringify("production"),
59 | }),
60 | nodeResolve({
61 | extensions,
62 | preferBuiltins: false,
63 | }),
64 | ],
65 | };
66 |
67 | export default [main, unpkg];
68 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | // constate(useCounter, value => value.count)
4 | // ^^^^^^^^^^^^^^^^^^^^
5 | type Selector = (value: Value) => any;
6 |
7 | // const [Provider, useCount, useIncrement] = constate(...)
8 | // ^^^^^^^^^^^^^^^^^^^^^^
9 | type SelectorHooks = {
10 | [K in keyof Selectors]: () => Selectors[K] extends (...args: any) => infer R
11 | ? R
12 | : never;
13 | };
14 |
15 | // const [Provider, useCounterContext] = constate(...)
16 | // or ^^^^^^^^^^^^^^^^^
17 | // const [Provider, useCount, useIncrement] = constate(...)
18 | // ^^^^^^^^^^^^^^^^^^^^^^
19 | type Hooks<
20 | Value,
21 | Selectors extends Selector[]
22 | > = Selectors["length"] extends 0 ? [() => Value] : SelectorHooks;
23 |
24 | // const [Provider, useContextValue] = constate(useValue)
25 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | type ConstateTuple[]> = [
27 | React.FC>,
28 | ...Hooks
29 | ];
30 |
31 | const isDev = process.env.NODE_ENV !== "production";
32 |
33 | const NO_PROVIDER = {};
34 |
35 | function createUseContext(context: React.Context): any {
36 | return () => {
37 | const value = React.useContext(context);
38 | if (isDev && value === NO_PROVIDER) {
39 | const warnMessage = context.displayName
40 | ? `The context consumer of ${context.displayName} must be wrapped with its corresponding Provider`
41 | : "Component must be wrapped with Provider.";
42 | // eslint-disable-next-line no-console
43 | console.warn(warnMessage);
44 | }
45 | return value;
46 | };
47 | }
48 |
49 | function constate[]>(
50 | useValue: (props: Props) => Value,
51 | ...selectors: Selectors
52 | ): ConstateTuple {
53 | const contexts = [] as React.Context[];
54 | const hooks = ([] as unknown) as Hooks;
55 |
56 | const createContext = (displayName: string) => {
57 | const context = React.createContext(NO_PROVIDER);
58 | if (isDev && displayName) {
59 | context.displayName = displayName;
60 | }
61 | contexts.push(context);
62 | hooks.push(createUseContext(context));
63 | };
64 |
65 | if (selectors.length) {
66 | selectors.forEach((selector) => createContext(selector.name));
67 | } else {
68 | createContext(useValue.name);
69 | }
70 |
71 | const Provider: React.FC> = ({
72 | children,
73 | ...props
74 | }) => {
75 | const value = useValue(props as Props);
76 | let element = children as React.ReactElement;
77 | for (let i = 0; i < contexts.length; i += 1) {
78 | const context = contexts[i];
79 | const selector = selectors[i] || ((v) => v);
80 | element = (
81 | {element}
82 | );
83 | }
84 | return element;
85 | };
86 |
87 | if (isDev && useValue.name) {
88 | Provider.displayName = "Constate";
89 | }
90 |
91 | return [Provider, ...hooks];
92 | }
93 |
94 | export default constate;
95 |
--------------------------------------------------------------------------------
/test/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { render, fireEvent } from "@testing-library/react";
3 | import constate from "../src";
4 |
5 | function useCounter({ initialCount = 0 } = {}) {
6 | const [count, setCount] = React.useState(initialCount);
7 | const increment = React.useCallback(() => setCount((c) => c + 1), []);
8 | const decrement = () => setCount(count - 1);
9 | return { count, increment, decrement };
10 | }
11 |
12 | test("no selectors", () => {
13 | const [CounterProvider, useCounterContext] = constate(useCounter);
14 | const Increment = () => {
15 | const { increment } = useCounterContext();
16 | return ;
17 | };
18 | const Count = () => {
19 | const { count } = useCounterContext();
20 | return {count}
;
21 | };
22 | const App = () => (
23 |
24 |
25 |
26 |
27 | );
28 | const { getByText } = render();
29 | expect(getByText("0")).toBeDefined();
30 | fireEvent.click(getByText("Increment"));
31 | expect(getByText("1")).toBeDefined();
32 | fireEvent.click(getByText("Increment"));
33 | expect(getByText("2")).toBeDefined();
34 | });
35 |
36 | test("single selector", () => {
37 | const [CounterProvider, useCount] = constate(
38 | useCounter,
39 | (value) => value.count
40 | );
41 | const Count = () => {
42 | const count = useCount();
43 | return {count}
;
44 | };
45 | const App = () => (
46 |
47 |
48 |
49 | );
50 | const { getByText } = render();
51 | expect(getByText("10")).toBeDefined();
52 | });
53 |
54 | test("two selectors", () => {
55 | const [CounterProvider, useCount, useIncrement] = constate(
56 | useCounter,
57 | (value) => value.count,
58 | (value) => value.increment
59 | );
60 | const Increment = () => {
61 | const increment = useIncrement();
62 | return ;
63 | };
64 | const Count = () => {
65 | const count = useCount();
66 | return {count}
;
67 | };
68 | const App = () => (
69 |
70 |
71 |
72 |
73 | );
74 | const { getByText } = render();
75 | expect(getByText("10")).toBeDefined();
76 | fireEvent.click(getByText("Increment"));
77 | expect(getByText("11")).toBeDefined();
78 | fireEvent.click(getByText("Increment"));
79 | expect(getByText("12")).toBeDefined();
80 | });
81 |
82 | test("two selectors with inline useValue", () => {
83 | const [CounterProvider, useCount, useIncrement] = constate(
84 | ({ initialCount = 0 }: { initialCount?: number } = {}) => {
85 | const [count, setCount] = React.useState(initialCount);
86 | const increment = React.useCallback(() => setCount((c) => c + 1), []);
87 | const decrement = () => setCount(count - 1);
88 | return { count, increment, decrement };
89 | },
90 | (value) => value.count,
91 | (value) => value.increment
92 | );
93 | const Increment = () => {
94 | const increment = useIncrement();
95 | return ;
96 | };
97 | const Count = () => {
98 | const count = useCount();
99 | return {count}
;
100 | };
101 | const App = () => (
102 |
103 |
104 |
105 |
106 | );
107 | const { getByText } = render();
108 | expect(getByText("10")).toBeDefined();
109 | fireEvent.click(getByText("Increment"));
110 | expect(getByText("11")).toBeDefined();
111 | fireEvent.click(getByText("Increment"));
112 | expect(getByText("12")).toBeDefined();
113 | });
114 |
115 | test("two selectors with hooks inside them", () => {
116 | const [CounterProvider, useCount, useDecrement] = constate(
117 | useCounter,
118 | (value) => value.count,
119 | (value) => React.useCallback(value.decrement, [value.count])
120 | );
121 | const Decrement = () => {
122 | const decrement = useDecrement();
123 | return ;
124 | };
125 | const Count = () => {
126 | const count = useCount();
127 | return {count}
;
128 | };
129 | const App = () => (
130 |
131 |
132 |
133 |
134 | );
135 | const { getByText } = render();
136 | expect(getByText("10")).toBeDefined();
137 | fireEvent.click(getByText("Decrement"));
138 | expect(getByText("9")).toBeDefined();
139 | fireEvent.click(getByText("Decrement"));
140 | expect(getByText("8")).toBeDefined();
141 | });
142 |
143 | test("without provider", () => {
144 | jest.spyOn(console, "warn").mockImplementation(() => {});
145 | const [, useCount] = constate(useCounter);
146 | const Count = () => {
147 | useCount();
148 | return null;
149 | };
150 | const App = () => ;
151 | render();
152 | // eslint-disable-next-line no-console
153 | expect(console.warn).toHaveBeenCalledWith(
154 | "The context consumer of useCounter must be wrapped with its corresponding Provider"
155 | );
156 | });
157 |
158 | test("without the provider which is created by anonymous function", () => {
159 | jest.spyOn(console, "warn").mockImplementation(() => {});
160 | const [, useCount] = constate(() => useCounter());
161 | const Count = () => {
162 | useCount();
163 | return null;
164 | };
165 | const App = () => ;
166 | render();
167 | // eslint-disable-next-line no-console
168 | expect(console.warn).toHaveBeenCalledWith(
169 | "Component must be wrapped with Provider."
170 | );
171 | });
172 |
173 | test("displayName with named useValue with no selector", () => {
174 | const [Provider] = constate(useCounter);
175 | expect(Provider.displayName).toBe("Constate");
176 | });
177 |
178 | test("displayName with anonymous useValue", () => {
179 | const [Provider] = constate(() => {});
180 | expect(Provider.displayName).toBeUndefined();
181 | });
182 |
183 | test("displayName with named useValue with selectors", () => {
184 | const [Provider] = constate(
185 | useCounter,
186 | // @ts-expect-error
187 | (value) => value.count,
188 | // @ts-expect-error
189 | (value) => value.increment
190 | );
191 | expect(Provider.displayName).toBe("Constate");
192 | });
193 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist/ts",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "strict": true,
9 | "declaration": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "noImplicitAny": true,
12 | "noImplicitReturns": true,
13 | "noImplicitThis": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "esModuleInterop": true,
17 | "importHelpers": true,
18 | "sourceMap": true,
19 | "removeComments": true,
20 | "stripInternal": true
21 | },
22 | "exclude": [
23 | "examples",
24 | "dist"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------