├── .babelrc
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── rollup.config.js
├── src
├── index.js
├── shallow-equals.js
└── warning.js
└── test
├── create-test-components.js
├── index.test.js
├── setup.js
├── shallow-equals.test.js
└── warning.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "build": {
4 | "presets": [["env", { "modules": false }], "stage-3", "react"],
5 | "plugins": ["external-helpers", "transform-class-properties"]
6 | },
7 | "buildProd": {
8 | "presets": [["env", { "modules": false }], "stage-3", "react"],
9 | "plugins": ["external-helpers", "transform-class-properties"]
10 | },
11 | "es": {
12 | "presets": [["env", { "modules": false }], "stage-3", "react"],
13 | "plugins": ["transform-class-properties"]
14 | },
15 | "commonjs": {
16 | "plugins": [
17 | ["transform-es2015-modules-commonjs", { "loose": true }],
18 | "transform-class-properties"
19 | ],
20 | "presets": ["stage-3", "react"]
21 | },
22 | "test": {
23 | "presets": ["env", "stage-3", "react"],
24 | "plugins": ["transform-class-properties"]
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | package-lock.json
64 | dist
65 | es
66 | lib
67 |
68 | npm-debug.log*
69 | yarn-debug.log*
70 | yarn-error.log*
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxBracketSameLine": true,
4 | "trailingComma": "es5"
5 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | sudo: false
5 | notifications:
6 | email: false
7 | after_success:
8 | # Upload to coveralls, but don't _fail_ if coveralls is down.
9 | - cat coverage/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed"
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### v1.0.0 (2019/2/12)
4 |
5 | No changes; this release marks API stability.
6 |
7 | ### v0.3.0 (2018/5/29)
8 |
9 | This update is largely a response to @gaearon's great feedback.
10 |
11 | * The Provider is now more performant. It will no longer render its Consumers
12 | when its parents rerender.
13 | * Another performance improvement: the Provider will only render its Consumers
14 | when the state value has changed (using a shallow equality check).
15 | * The actions API is simpler, drawing inspiration from redux-thunk. Note that
16 | this is a breaking change, although no functionality has been removed.
17 | * State must now be an object or null, like a React component's state.
18 | * New, helpful warnings have been introduced.
19 |
20 | ### v0.2.1 (2018/5/25)
21 |
22 | * Fixes a bug where calling `setState` would not work as intended.
23 |
24 | ### v0.2.0 (2018/5/25)
25 |
26 | * Refactor the API to no longer bind actions to the Component instance.
27 |
28 | ### v0.1.0 (2018/5/25)
29 |
30 | * Expand the React `peerDependency` range to include v0.14.0.
31 |
32 | ### v0.0.2 (2018/5/25)
33 |
34 | This was the first release of the library.
35 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jamesplease2@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | > :wave: Hey there! Thanks for your interest in helping out. If you happen to
4 | > run into any issues following this guide, please
5 | > [open an issue](https://github.com/jamesplease/react-state-context/issues/new?title=Contributing+help),
6 | > and we'll do our best to help out.
7 |
8 | To begin contributing, you'll first need to
9 | [clone this repository](https://help.github.com/articles/cloning-a-repository/),
10 | then navigate into the repository's directory.
11 |
12 | ```sh
13 | git clone git@github.com:jamesplease/react-state-context.git
14 |
15 | cd react-state-context
16 | ```
17 |
18 | Next, install the dependencies using [npm](https://www.npmjs.com/).
19 |
20 | ```js
21 | npm install
22 | ```
23 |
24 | ### Contributing to the Code
25 |
26 | The source files can be found in `./src`. As you work, and/or when you're done, run
27 | `npm test` to run the unit tests.
28 |
29 | Once you're done, go ahead and open a Pull Request.
30 |
31 | > :information_desk_person: Don't sweat it if you can't get the tests to pass,
32 | > or if you can't finish the changes you'd like to make. If you still open up a
33 | > Pull Request, we'll make sure it gets figured out.
34 |
35 | ### One More Thing...
36 |
37 | Thanks again!
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 James
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 | # React State Context
2 |
3 | [](https://travis-ci.org/jamesplease/react-state-context)
4 | [](https://www.npmjs.com/package/react-state-context)
5 | [](https://coveralls.io/github/jamesplease/react-state-context?branch=master)
6 | [](https://unpkg.com/react-state-context/dist/react-state-context.min.js)
7 |
8 | Lightweight state management using React Context.
9 |
10 | ✔ Built on React primitives
11 | ✔ Provides a familiar API
12 | ✔ Designed with a minimal learning curve in mind
13 | ✔ Reasonable file size (~2kb gzipped)
14 |
15 | ## Motivation
16 |
17 | When you are getting started with React, storing all of your application state within individual Components'
18 | [state](https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class) tends to work well.
19 | Component state is a good solution for storing data.
20 |
21 | A limitation of component state is that it can be tedious to share it between components
22 | that are not near one another within your application's component tree. This problem may become more pronounced
23 | as your application grows, and as some of your data needs to be made available to a larger number of separated
24 | components.
25 |
26 | React provides an API to solve this problem: **[Context](https://reactjs.org/docs/context.html)**. Context is a
27 | mechanism to more easily share data between components, even when they are not close.
28 |
29 | As delightful as the Context API is, it is a low-level tool, so using it directly can be a little verbose sometimes.
30 | It also doesn't provide opinions on _how_ it should be used, so it can take some time to figure out an organized system for
31 | working with it. Lastly, it has [some caveats](https://reactjs.org/docs/context.html#caveats) that can trip you up. For these
32 | reasons I created **React State Context**.
33 |
34 | React State Context is a thin wrapper around Context that provides a small amount of structure. This structure
35 | helps reduce the boilerplate that you must write, and it also helps you to stay organized. Plus, when you use State
36 | Context, you can be confident that you are avoiding the caveats that accompany using Context directly.
37 |
38 | ### Installation
39 |
40 | Install using [npm](https://www.npmjs.com):
41 |
42 | ```
43 | npm install react-state-context
44 | ```
45 |
46 | or [yarn](https://yarnpkg.com/):
47 |
48 | ```
49 | yarn add react-state-context
50 | ```
51 |
52 | ## Concepts
53 |
54 | React State Context has three concepts.
55 |
56 | ### StateContext
57 |
58 | A StateContext is a wrapper around a normal React Context object. Like a regular Context object, it has two properties:
59 | `Provider` and `Consumer`.
60 |
61 | You use StateContexts in the same way as regular Context. If you have used the new Context API, then it should feel
62 | familiar to use StateContexts. If you haven't, don't worry. If I was able to learn it, then you can, too!
63 |
64 | > :information_desk_person: The [React documentation on Context](https://reactjs.org/docs/context.html#when-to-use-context)
65 | > is a great resource. It can be helpful to familiarize yourself with the content on that page before using State Context.
66 |
67 | What is different about a StateContext is that the value that the Consumer provides you with has the
68 | following structure:
69 |
70 | ```
71 | {
72 | state,
73 | ...actions
74 | }
75 | ```
76 |
77 | State and actions are the other two concepts of React State Context. Let's take a look!
78 |
79 | ### State
80 |
81 | Every StateContext object has an internal state object. Behind the scenes, it is a regular Component's state object. When you render
82 | a `StateContext.Consumer`, the value passed to the render prop will include a `state` attribute.
83 |
84 | ```jsx
85 |
86 | {value => {
87 | console.log('The current state is:', value.state);
88 | }}
89 |
90 | ```
91 |
92 | Like a React Component's state, the StateContext state must be an object or null.
93 |
94 | ### Actions
95 |
96 | Actions are functions that you define, and they are how you modify the state. If you have used Redux, then you can
97 | think of them as serving a similar role to action creators.
98 |
99 | To update state, return a new value from your action. The returned value will be shallowly merged
100 | with the existing state.
101 |
102 | Let's take a look at an example action:
103 |
104 | ```js
105 | export function openModal() {
106 | return {
107 | isOpen: true,
108 | };
109 | }
110 | ```
111 |
112 | When you call an action from within your application, you can pass arguments to it. Let's use this to
113 | create an action to toggle a modal state based on what is passed into the action:
114 |
115 | ```js
116 | export function toggleModal(isOpen) {
117 | return { isOpen };
118 | }
119 | ```
120 |
121 | Sometimes, you may need the previous state within an action. In these situations, you can return a
122 | function from your action. This function will be called with one argument, `setState`. Use `setState` to update
123 | the state as you would using a React Component's `setState`:
124 |
125 | ```js
126 | export function createTodo(newTodo) {
127 | return function(setState) {
128 | setState(prevState => {
129 | return {
130 | todos: prevState.todos.concat([newTodo])
131 | );
132 | });
133 | };
134 | }
135 | ```
136 |
137 | Note that `setState` differs from the Component `setState` in that there is no second argument.
138 |
139 | > :information_desk_person: Heads up! The actions API was inspired by [redux-thunk](https://github.com/reduxjs/redux-thunk).
140 | > If you have used that API, you may notice the similarity. In redux-thunk, the thunks are passed the arguments `(dispatch, getState)`.
141 | > In this library, you are passed `(setState)`.
142 |
143 | Along with `state`, the actions that you define will be included in the `value` that you receive from the Consumer:
144 |
145 | ```jsx
146 |
147 | {value => {
148 | console.log('I can add a todo using:', value.createTodo);
149 | console.log('I can delete todos using:', value.deleteTodo);
150 | }}
151 |
152 | ```
153 |
154 | Once you feel comfortable with these concepts, you are ready to start using React State Context.
155 |
156 | ## API
157 |
158 | This library has one, default export: `createStateContext`.
159 |
160 | ### `createStateContext( actions, [initialState] )`
161 |
162 | Creates and returns a [StateContext](#state-context).
163 |
164 | * `actions` _[Object]_: The actions that modify the state.
165 | * `[initialState]` _[Object|null]_: Optional initial state for the StateContext.
166 |
167 | ```js
168 | import createStateContext from 'react-state-context';
169 | import * as todoActions from './actions';
170 |
171 | const TodoContext = createStateContext(todoActions, {
172 | todos: [],
173 | });
174 |
175 | export default TodoContext;
176 | ```
177 |
178 | Use a StateContext as you would any other Context.
179 |
180 | ```jsx
181 | import TodoContext from './contexts/todo';
182 |
183 | export function App() {
184 | // To begin, you must render the Provider somewhere high up in the Component tree.
185 | return (
186 |
187 |
188 |
189 | );
190 | }
191 | ```
192 |
193 | ```jsx
194 | import TodoContext from './contexts/todo';
195 |
196 | export function DeeplyNestedChild() {
197 | // From anywhere within the Provider, you can access the value of the StateContext.
198 | return (
199 |
200 | {value => {
201 | console.log('The current state is', value.state);
202 | console.log('All of the todos are here', value.state.todos);
203 |
204 | console.log('I can add a todo using:', value.createTodo);
205 | console.log('I can delete todos using:', value.deleteTodo);
206 | }}
207 |
208 | );
209 | }
210 | ```
211 |
212 | ## FAQ
213 |
214 | #### What version of React is required?
215 |
216 | You need to be using at least React v0.14.
217 |
218 | Although the new Context API was introduced in 16.3.0, this library is built using the excellent
219 | [create-react-context](https://github.com/jamiebuilds/create-react-context) library, which polyfills the
220 | API for older React versions.
221 |
222 | #### Why would someone use this over Redux?
223 |
224 | The reason that I initially started using Redux was to more easily share data between components. Although Redux can seem like a simple system once you become
225 | familiar with it, the number of concepts it has can make it daunting to newcomers. At least, that's how I felt when I was learning it.
226 |
227 | For me, React State Context solves the problems that I originally used Redux for in a more straightforward way, which is why I think a solution like this seems promising.
228 |
229 | #### What does one lose by migrating away from Redux?
230 |
231 | The Redux library supports middleware, and it enables time travel debugging, which are both things that you do not get from React State Context. If you
232 | rely heavily on those features of Redux, then this library may not be suitable for your needs.
233 |
234 | Outside of the Redux source code itself, there is an enormous community around that library. There are considerable benefits to using a library that has such a
235 | large number of users, and you will lose that community by switching to this library, or most other alternative state management libraries, for that matter.
236 |
237 | With that said, React State Context is built on top of React's built-in Context API. Although the new Context API is likely not very familiar to most React
238 | developers today, we believe that that will change as time goes on.
239 |
240 | #### How is this different from Unstated?
241 |
242 | [Unstated](https://github.com/jamiebuilds/unstated) is a fantastic library, and it served as inspiration for this library. The primary difference is that
243 | Unstated introduces new concepts for state management, like `Container` and `Subscribe`. One of the design goals of React State Context was to avoid
244 | introducing additional concepts whenever possible in an effort to reduce the learning curve.
245 |
246 | We believe that we avoided introducing those new concepts while still getting a remarkably similar developer experience. Perhaps you will
247 | feel the same way!
248 |
249 | ## Contributing
250 |
251 | Are you interested in helping out with this project? That's awesome – thank you! Head on over to
252 | [the contributing guide](./CONTRIBUTING.md) to get started.
253 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverage: true,
3 | collectCoverageFrom: ['src/**/*.{js,jsx}', '!**/node_modules/**'],
4 | coverageDirectory: 'coverage',
5 | setupTestFrameworkScriptFile: './test/setup.js',
6 | testURL: 'http://localhost'
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-state-context",
3 | "version": "1.0.0",
4 | "description": "Lightweight state management using React Context.",
5 | "main": "lib/index.js",
6 | "module": "es/index.js",
7 | "scripts": {
8 | "clean": "rimraf dist es tmp lib",
9 | "test": "jest",
10 | "prepublish": "in-publish && npm run build || not-in-publish",
11 | "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs",
12 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib",
13 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es",
14 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/react-state-context.js",
15 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=buildProd rollup -c -i src/index.js -o dist/react-state-context.min.js"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/jamesplease/react-state-context.git"
20 | },
21 | "keywords": [
22 | "react",
23 | "state",
24 | "context",
25 | "render",
26 | "prop",
27 | "state",
28 | "children",
29 | "redux",
30 | "flux",
31 | "action",
32 | "data"
33 | ],
34 | "author": "James Smith ",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/jamesplease/react-state-context/issues"
38 | },
39 | "files": [
40 | "dist",
41 | "lib",
42 | "es"
43 | ],
44 | "peerDependencies": {
45 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0"
46 | },
47 | "devDependencies": {
48 | "babel-cli": "^6.26.0",
49 | "babel-core": "^6.26.0",
50 | "babel-jest": "^22.1.0",
51 | "babel-plugin-external-helpers": "^6.22.0",
52 | "babel-plugin-transform-class-properties": "^6.24.1",
53 | "babel-plugin-transform-react-remove-prop-types": "^0.4.13",
54 | "babel-preset-env": "^1.6.1",
55 | "babel-preset-react": "^6.24.1",
56 | "babel-preset-stage-3": "^6.24.1",
57 | "coveralls": "^3.0.0",
58 | "cross-env": "^5.1.3",
59 | "in-publish": "^2.0.0",
60 | "jest": "^22.1.4",
61 | "jest-dom": "^1.3.0",
62 | "react": "^16.2.0",
63 | "react-dom": "^16.2.0",
64 | "react-test-renderer": "^16.2.0",
65 | "react-testing-library": "^3.1.3",
66 | "rimraf": "^2.6.2",
67 | "rollup": "^0.57.1",
68 | "rollup-plugin-babel": "^3.0.3",
69 | "rollup-plugin-commonjs": "^9.1.0",
70 | "rollup-plugin-node-resolve": "^3.3.0",
71 | "rollup-plugin-replace": "^2.0.0",
72 | "rollup-plugin-uglify": "^3.0.0"
73 | },
74 | "dependencies": {
75 | "create-react-context": "^0.2.2",
76 | "prop-types": "^15.6.1"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from 'rollup-plugin-node-resolve';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import babel from 'rollup-plugin-babel';
4 | import uglify from 'rollup-plugin-uglify';
5 | import replace from 'rollup-plugin-replace';
6 |
7 | var env = process.env.NODE_ENV;
8 | var config = {
9 | output: {
10 | name: 'createStateContext',
11 | globals: {
12 | react: 'React'
13 | },
14 | format: 'umd'
15 | },
16 | external: ['react'],
17 | context: 'this',
18 | plugins: [
19 | nodeResolve({
20 | jsnext: true
21 | }),
22 | commonjs({
23 | include: 'node_modules/**'
24 | }),
25 | babel({
26 | exclude: 'node_modules/**'
27 | }),
28 | replace({
29 | 'process.env.NODE_ENV': JSON.stringify(env)
30 | })
31 | ]
32 | };
33 |
34 | if (env === 'production') {
35 | config.plugins.push(
36 | uglify({
37 | compress: {
38 | pure_getters: true,
39 | unsafe: true,
40 | unsafe_comps: true,
41 | warnings: false
42 | }
43 | })
44 | );
45 | }
46 |
47 | export default config;
48 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import createReactContext from 'create-react-context';
3 | import shallowEquals from './shallow-equals';
4 | import { warning } from './warning';
5 |
6 | function isValidState(state) {
7 | if ((state && typeof state !== 'object') || Array.isArray(state)) {
8 | return false;
9 | } else {
10 | return true;
11 | }
12 | }
13 |
14 | export default function createStateContext(actions = {}, initialState) {
15 | const Context = createReactContext();
16 |
17 | let initialStateToUse;
18 | if (!isValidState(initialState)) {
19 | if (process.env.NODE_ENV !== 'production') {
20 | warning(
21 | `Warning: StateContext state must be an object or null. You passed an invalid value to createStateContext` +
22 | ` that has been ignored.`,
23 | 'INVALID_INITIAL_STATE'
24 | );
25 | }
26 | } else {
27 | initialStateToUse = initialState;
28 | }
29 |
30 | if (process.env.NODE_ENV !== 'production') {
31 | if ((actions && typeof actions !== 'object') || Array.isArray(actions)) {
32 | warning(
33 | `You passed invalid actions to createStateContext. actions must be an object.`,
34 | 'INVALID_ACTIONS_ARGUMENT'
35 | );
36 | }
37 | }
38 |
39 | class ProviderComponent extends Component {
40 | render() {
41 | const { children } = this.props;
42 |
43 | return ;
44 | }
45 |
46 | constructor(...args) {
47 | super(...args);
48 |
49 | let actionsToUse = {};
50 | for (let key in actions) {
51 | const action = actions[key];
52 |
53 | if (typeof action !== 'function') {
54 | if (process.env.NODE_ENV !== 'production') {
55 | warning(
56 | `Warning: an action with key ${key} was passed to createStateContext that was not a function. Actions` +
57 | ` must be functions. The ${key} action has been ignored. You should check your call to createStateContext().`,
58 | 'ACTION_MUST_BE_FN'
59 | );
60 | }
61 |
62 | continue;
63 | }
64 |
65 | if (key === 'state') {
66 | if (process.env.NODE_ENV !== 'production') {
67 | warning(
68 | `Warning: an action was passed to createStateContext with the key "state". This is a reserved key,` +
69 | ` so your action has been ignored.`,
70 | 'INVALID_ACTION_KEY'
71 | );
72 | }
73 |
74 | continue;
75 | }
76 |
77 | actionsToUse[key] = (...args) => this.onAction(action(...args));
78 | }
79 |
80 | // The Provider's `value` is this Component's state.
81 | // From the API of ReactStateContext, we know that a Provider's value has the form:
82 | //
83 | // {
84 | // state,
85 | // ...actions
86 | // }
87 | //
88 | // Together, those two facts lead to there being a state attribute on this Component's
89 | // state.
90 | this.state = {
91 | state: initialStateToUse,
92 | ...actionsToUse,
93 | };
94 | }
95 |
96 | onAction = returnValue => {
97 | const returnValueType = typeof returnValue;
98 |
99 | // If the value is undefined, then we have no update to make.
100 | if (returnValueType === 'undefined') {
101 | return;
102 | }
103 |
104 | // If they pass a function, then the action is a thunk. We pass them
105 | // the setState wrapper.
106 | else if (returnValueType === 'function') {
107 | returnValue(this.setStateWrapper);
108 | }
109 |
110 | // If it not undefined, nor a function, nor valid, then we log a warning and do nothing.
111 | else if (!isValidState(returnValue)) {
112 | if (process.env.NODE_ENV !== 'production') {
113 | warning(
114 | `Warning: StateContext actions must update state to an object or null. You called an action that` +
115 | ` set an invalid value. This value has been ignored, and the state has not been updated.`,
116 | 'INVALID_ACTION_UPDATE'
117 | );
118 | }
119 |
120 | return;
121 | }
122 |
123 | // The last condition is if they return a plain object.
124 | // In that situation, we set the state after merging it.
125 | else {
126 | this.setState(prevState => {
127 | const merged = this.getUpdatedState(prevState, returnValue);
128 | return merged;
129 | });
130 | }
131 | };
132 |
133 | getUpdatedState = (prevState, newState) => {
134 | if (!isValidState(newState)) {
135 | if (process.env.NODE_ENV !== 'production') {
136 | warning(
137 | `Warning: StateContext actions must update state to an object or null. You called an action that` +
138 | ` set an invalid value. This value has been ignored, and the state has not been updated.`,
139 | 'INVALID_ACTION_UPDATE'
140 | );
141 | }
142 |
143 | return;
144 | }
145 |
146 | // To compute the _potential_ new state, we shallow merge the two.
147 | let mergedState =
148 | newState === null ? null : Object.assign({}, prevState.state, newState);
149 |
150 | // If the previous value and the new value are shallowly equal, then we avoid the update altogether.
151 | // In this way, a StateContext.Provider behaves similarly to a PureComponent.
152 | if (shallowEquals(prevState.state, mergedState)) {
153 | return;
154 | }
155 |
156 | return {
157 | state: mergedState,
158 | };
159 | };
160 |
161 | // This function is what is called when you call an action.
162 | // `stateUpdate` is just similar to the first argument of `setState`, in that it can be
163 | // a function that is passed the previous state, or an update object.
164 | setStateWrapper = stateUpdate => {
165 | this.setState(prevState => {
166 | // To compute the new state to merge with the old, we mimic the behavior of a Component's
167 | // `setState`, allowing you to pass either an object or a function.
168 | const newState =
169 | typeof stateUpdate === 'function'
170 | ? stateUpdate(prevState.state)
171 | : stateUpdate;
172 |
173 | return this.getUpdatedState(prevState, newState);
174 | });
175 | };
176 | }
177 |
178 | return {
179 | Provider: ProviderComponent,
180 | Consumer: Context.Consumer,
181 | };
182 | }
183 |
--------------------------------------------------------------------------------
/src/shallow-equals.js:
--------------------------------------------------------------------------------
1 | export default function shallowEquals(a, b) {
2 | // Handle exact object matches and primitives
3 | if (a === b) {
4 | return true;
5 | }
6 |
7 | // When either value are null, then a strict equals comparison will return
8 | // the expected value.
9 | if (a === null || b === null) {
10 | return a === b;
11 | }
12 |
13 | const aKeys = Object.keys(a);
14 | const bKeys = Object.keys(b);
15 |
16 | // If they are both objects, then they must have the same
17 | // number of keys. Otherwise, they can't be shallowly equal!
18 | if (aKeys.length !== bKeys.length) {
19 | return false;
20 | }
21 |
22 | for (var prop in b) {
23 | if (a[prop] !== b[prop]) {
24 | return false;
25 | }
26 | }
27 |
28 | return true;
29 | }
30 |
--------------------------------------------------------------------------------
/src/warning.js:
--------------------------------------------------------------------------------
1 | let codeCache = {};
2 |
3 | export function warning(message, code) {
4 | // This ensures that each warning type is only logged out one time
5 | if (code) {
6 | if (codeCache[code]) {
7 | return;
8 | }
9 |
10 | codeCache[code] = true;
11 | }
12 |
13 | if (console && typeof console.error === 'function') {
14 | console.error(message);
15 | }
16 |
17 | try {
18 | // This error was thrown as a convenience so that if you enable
19 | // "break on all exceptions" in your console,
20 | // it would pause the execution at this line.
21 | throw new Error(message);
22 | } catch (e) {
23 | // Intentionally blank
24 | }
25 | }
26 |
27 | export function resetCodeCache() {
28 | codeCache = {};
29 | }
30 |
--------------------------------------------------------------------------------
/test/create-test-components.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createStateContext from '../src';
3 |
4 | const defaultActions = {
5 | setValue() {
6 | return {
7 | number: 10,
8 | };
9 | },
10 |
11 | increment: () => setState => {
12 | setState(prevState => {
13 | return {
14 | number: prevState.number + 1,
15 | };
16 | });
17 | },
18 |
19 | updateToTheSame: () => setState => {
20 | setState(prevState => {
21 | return {
22 | number: prevState.number,
23 | };
24 | });
25 | },
26 |
27 | badAction() {
28 | return [];
29 | },
30 |
31 | badThunkAction: () => setState => {
32 | setState([]);
33 | },
34 |
35 | returnsUndefined() {},
36 | };
37 |
38 | export default function createTestComponents(
39 | actions = defaultActions,
40 | initialNumber = 2
41 | ) {
42 | const initialState = {
43 | number: initialNumber,
44 | };
45 |
46 | const StateContext = createStateContext(actions, initialState);
47 |
48 | const Usage = () => (
49 |
50 | {value => {
51 | return (
52 |
53 | The number is: {value.state.number}
54 |
55 |
56 |
57 |
58 |
59 |
60 |