├── .babelrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── package.json
├── src
└── index.js
├── tests
├── create-multi-context.spec.js
└── static-with.spec.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "@babel/preset-env", "@babel/preset-react" ],
3 | "plugins": [
4 | "@babel/plugin-proposal-class-properties",
5 | "@babel/plugin-proposal-object-rest-spread"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.git
2 | /.vscode
3 | /node_modules
4 | /index.js
5 | /yarn-error.log
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.git
2 | /.vscode
3 | /node_modules
4 | /src
5 | /.babelrc
6 | /.gitignore
7 | /.npmignore
8 | /package-lock.json
9 | /webpack.config.js
10 | /yarn.lock
11 | /yarn-error.log
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 8.9.1
3 | branches:
4 | only: master
5 | cache: yarn
6 | install: yarn
7 | script:
8 | - yarn build
9 | - yarn test
10 | deploy:
11 | api_key:
12 | secure: gtZp8ZsBkkxghvId4L/FQK5SwsIc7zOt2wU3VLqaPLp0bTMaXVrqH5fFrjSr1KcMw65jv/SWI1pF1n5Sah3nd6poGdLSTDGKK+yuOP7OlwrjA/5BtPiWqkjb2Ou7RpgvUkUvHwA+VzrZfRQ+CjYCd9oY+kwJw1DqF1P7DlA8kLEZ7xmOvwF8L9laDV9l/V0kdmdw7fhhG7aYI/4UxQA8xw/kJYpVtfcJcIMSCNYGlN5x6XwoGL3hZD8wOnxvxUjhvLd6B+oKILBNM9QI+C4yB6LrryH99CNrydBSte3H4pTUwK4CoAx4l9kzYJqKSDjp1PEpnxntBDx9MjUVoLWvIPZzdzt6ZefSIuw5AJzpbCi535ET0k1UE2QAVr/FhFd1N7daYGEMMhl5Qb+5tvLWVLB2w+giXCwOylNaYBiwOIYNldfPOImZG4K3EV1ktO5OHdNkR9DHfztfurkahrfSzTjlBSo98SI15GOzea/E+RWuQVQbpOuAZQzCRoHQ6VxxazUZJKJy7qaTWdG+j4oWutAnB1PQUVrPbw5RglFvHmpigjhkTyv/6Y0Vb39aQUp1fGh9A9fnyuD9/wvcw5F9TsNpq20MHulqRf08ixc6P0uBiq8aVKonS7yed0gefUeKfkyJ2lXwSe4nBneZtTc2QNarEyt64mY7RxoB0/rAcWM=
13 | email: npmjs@charlesstover.com
14 | on:
15 | branch: master
16 | provider: npm
17 | skip_cleanup: true
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Charles Stover
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 Multi-Context [](https://twitter.com/intent/tweet?text=Simplify%20React's%20Context%20API%20by%20managing%20multiple%20contexts%20with%20a%20single%20component.&url=https://github.com/CharlesStover/react-multi-context&via=CharlesStover&hashtags=react,reactjs,javascript,webdev,webdeveloper,webdevelopment)
2 |
3 | Manage multiple contexts with a single React component.
4 |
5 | [](https://www.npmjs.com/package/react-multi-context)
6 | [](https://www.npmjs.com/package/react-multi-context)
7 | [](https://www.npmjs.com/package/react-multi-context)
8 | [](https://www.npmjs.com/package/react-multi-context)
9 | [](https://travis-ci.com/CharlesStover/react-multi-context/)
10 |
11 | ## Install
12 |
13 | * `npm install react-multi-context --save` or
14 | * `yarn add react-multi-context`
15 |
16 | ## Test
17 |
18 | * `npm run test` or
19 | * `yarn test`
20 |
21 | ## Use
22 |
23 | ```JS
24 | import createMultiContext from 'react-multi-context';
25 | export const MyMultiContext = createMultiContext();
26 | ```
27 |
28 | Create the context by importing and executing `createMultiContext` wherever you want to create context.
29 | Then, import that multi-context instance as needed.
30 |
31 | Use the `set` prop to set a context's value.
32 | Changing the value on a key-value pair in a context will cause all getters for that key to re-render.
33 |
34 | Use the `get` prop to get a context's value.
35 | Using this prop will execute the `children` render prop by passing the corresponding values of the context as the parameters.
36 |
37 | ## Example
38 |
39 | ```JS
40 | // Parent.js
41 | import createMultiContext from 'react-multi-context';
42 |
43 | // Context to provide to children.
44 | export const ParentContext = createMultiContext();
45 |
46 | export default class Parent extends React.Component {
47 | render() {
48 | return (
49 |
59 |
60 |
61 | );
62 | }
63 | }
64 | ```
65 | ```JS
66 | // Child.js
67 | // Get the context provided from the parent
68 | import { ParentContext } from './Parent';
69 |
70 | export default class Child extends React.Component {
71 | render() {
72 | return (
73 |
74 | {(project, user) =>
75 |
76 | /* This is a demo of React Multi-Context v1.0 by Charles! */
77 | This is a demo of {project.name} v{project.version} by {user}!
78 | }
79 |
80 | );
81 | }
82 | }
83 | ```
84 |
85 | ## Example (Shorter)
86 |
87 | ```JS
88 | // Parent - writes A and B
89 | const Parent = () =>
90 |
91 |
92 |
93 |
94 | ;
95 | ```
96 | ```JS
97 | // Child1 - reads A
98 | // Note: Each value is its own context, which is what makes this MULTI-context.
99 | const Child1 = () =>
100 |
101 | {(a) => `The value of A is ${a}!`}
102 | ;
103 | ```
104 | ```JS
105 | // Child2 - reads A and B
106 | // Note: Reading multiple values will trigger a re-render if any one read value changes.
107 | const Child2 = () =>
108 |
109 | {(a, b) => `The value of A+B is ${a + b}!`}
110 | ;
111 | ```
112 | ```JS
113 | // Child3 - reads B and A
114 | // Note: The order of the get prop corresponds to the order of the function parameters.
115 | const Child3 = () =>
116 |
117 | {(b, a) => `The value of A+B is ${a + b}!`}
118 | ;
119 | ```
120 |
121 | ## Default Values
122 |
123 | You may pass an object of default values for the contexts as a parameter to `createMultiContext` or via the `default` prop.
124 |
125 | ```JS
126 | const MyMultiContext = createMultiContext({ a: 0, b: 0 });
127 | ```
128 |
129 | or
130 |
131 | ```JS
132 |
136 |
137 | {b => 'I predict that B equals zero: ' + b}
138 |
139 |
140 | ```
141 |
142 | You do not need to do both.
143 |
144 | ## MultiContext.with
145 |
146 | `MultiContextInstance.with(...multiContextKeys)(Component)` will bind the `multiContextKeys` of `MultiContextInstance` to the props of `Component`.
147 |
148 | ```JS
149 | import React from 'react';
150 | import { SomeMultiContext } from './some-component';
151 |
152 | class MyComponent extends React.PureComponent {
153 | render() {
154 | return
;
155 | }
156 | }
157 |
158 | // Binds the MultiContext's `name` property to MyComponent's `name` prop.
159 | export default SomeMultiContext.with('name', 'age')(MyComponent);
160 | ```
161 |
162 | ## Sponsor 💗
163 |
164 | If you are a fan of this project, you may
165 | [become a sponsor](https://github.com/sponsors/CharlesStover)
166 | via GitHub's Sponsors Program.
167 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-multi-context",
3 | "version": "1.1.2",
4 | "author": "Charles Stover",
5 | "bugs": {
6 | "url": "https://github.com/CharlesStover/react-multi-context/issues"
7 | },
8 | "description": "Manage multiple contexts with a single React component.",
9 | "devDependencies": {
10 | "@babel/core": "^7.0.0-beta.53",
11 | "@babel/plugin-proposal-class-properties": "^7.0.0-rc.0",
12 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.53",
13 | "@babel/preset-env": "^7.0.0-beta.53",
14 | "@babel/preset-react": "^7.0.0-beta.54",
15 | "babel-core": "^7.0.0-bridge.0",
16 | "babel-jest": "^23.4.0",
17 | "babel-loader": "^8.0.0-beta.4",
18 | "enzyme": "^3.3.0",
19 | "enzyme-adapter-react-16": "^1.2.0",
20 | "jest": "^24.8.0",
21 | "react": "^16.0.0",
22 | "react-dom": "^16.4.1",
23 | "webpack": "^4.16.0",
24 | "webpack-cli": "^3.0.8"
25 | },
26 | "homepage": "https://github.com/CharlesStover/react-multi-context#readme",
27 | "keywords": [
28 | "react",
29 | "reactjs",
30 | "context"
31 | ],
32 | "license": "MIT",
33 | "main": "index.js",
34 | "peerDependencies": {
35 | "react": "^16.0.0"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/CharlesStover/react-multi-context.git"
40 | },
41 | "scripts": {
42 | "build": "webpack",
43 | "prepublishOnly": "npm run build",
44 | "test": "jest tests/.+.spec.js"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const providerSort = (a, b) =>
4 | a[0] < b[0] ?
5 | -1 :
6 | 1;
7 |
8 | const createMultiContext = (defaults = Object.create(null)) => {
9 | const contexts = Object.create(null);
10 | for (const [ key, value ] of Object.entries(defaults)) {
11 | contexts[key] = React.createContext(value);
12 | }
13 |
14 | return class MultiContext extends React.PureComponent {
15 |
16 | static with(...contextKeys) {
17 |
18 | const reduceContextToProps = (props, contextValue, index) => {
19 | props[contextKeys[index]] = contextValue;
20 | return props;
21 | };
22 |
23 | return (Component) =>
24 | class WithMultiContext extends React.PureComponent {
25 |
26 | contextConsumer = (...contextValues) => {
27 | const props = contextValues.reduce(
28 | reduceContextToProps,
29 | Object.create(null)
30 | );
31 | return (
32 |
36 | );
37 | }
38 |
39 | render() {
40 | return (
41 |
45 | );
46 | }
47 | };
48 | }
49 |
50 | createContext(key) {
51 | if (!Object.prototype.hasOwnProperty.call(contexts, key)) {
52 | contexts[key] =
53 | typeof this.props.default === 'object' &&
54 | this.props.default !== null &&
55 | Object.prototype.hasOwnProperty.call(this.props.default, key) ?
56 | React.createContext(this.props.default[key]) :
57 | React.createContext();
58 | }
59 | return contexts[key];
60 | }
61 |
62 | // Recursively generate , with each consumer being a child of the last.
63 | getConsumer(children, index, values) {
64 |
65 | // If we have all the consumers, we can spread the values to the child render prop.
66 | if (this.props.get.length === index) {
67 | return children(...values);
68 | }
69 |
70 | // Check if this context exists.
71 | const contextKey = this.props.get[index];
72 | if (!Object.prototype.hasOwnProperty.call(contexts, contextKey)) {
73 | throw new Error('Context `' + contextKey + '` does not exist.');
74 | }
75 |
76 | // Create this context's consumer.
77 | const Context = contexts[contextKey];
78 | return (
79 |
82 | this.getConsumer(
83 | children,
84 | index + 1,
85 | values.concat([ value ])
86 | )
87 | }
88 | />
89 | );
90 | }
91 |
92 | getProvider(children, providers, index) {
93 |
94 | // If we have all the consumers, we can spread the values to the child render prop.
95 | if (providers.length === index) {
96 | return children;
97 | }
98 |
99 | // Create the context (if it doesn't exist).
100 | const Context = this.createContext(providers[index][0]);
101 |
102 | // Create this context's provider.
103 | return (
104 |
108 | );
109 | }
110 |
111 | get hasConsumers() {
112 | return (
113 | Array.isArray(this.props.get) &&
114 | this.props.get.length > 0
115 | );
116 | }
117 |
118 | get hasProviders() {
119 | return (
120 | typeof this.props.set === 'object' &&
121 | this.props.set !== null
122 | );
123 | }
124 |
125 | renderConsumers(children) {
126 |
127 | // If there are no consumers to get, just return the children.
128 | if (!this.hasConsumers) {
129 | return children;
130 | }
131 |
132 | // If there are consumers to get, get them one at a time.
133 | if (typeof children !== 'function') {
134 | throw new Error('Consuming context requires a function child.');
135 | }
136 |
137 | return this.getConsumer(children, 0, []);
138 | }
139 |
140 | renderProviders(children) {
141 |
142 | // If there are no providers to set, just return the children.
143 | if (!this.hasProviders) {
144 | return children;
145 | }
146 |
147 | // If there are providers to set,
148 | const providers = Object.entries(this.props.set);
149 | if (
150 | typeof this.props.default === 'object' &&
151 | this.props.default !== null
152 | ) {
153 | const propDefaults = Object.entries(this.props.default);
154 | const propDefaultsLength = propDefaults.length;
155 | for (let x = 0; x < propDefaultsLength; x++) {
156 |
157 | // Don't duplicate a provider if it is already set.
158 | if (!Object.prototype.hasOwnProperty.call(this.props.set, propDefaults[x][0])) {
159 | providers.push(propDefaults[x]);
160 | }
161 | }
162 | }
163 | if (providers.length === 0) {
164 | return children;
165 | }
166 |
167 | // Sort them to insure same hierarchy and prevent unnecessary re-rendering (if hierarchy were to change).
168 | providers.sort(providerSort);
169 |
170 | // If there are consumers, return a function render prop that sets each provider one at a time.
171 | if (this.hasConsumers) {
172 | return (...values) => this.getProvider(children(...values), providers, 0);
173 | }
174 |
175 | // If there are no consumers, set each provider one at a time.
176 | return this.getProvider(children, providers, 0);
177 | }
178 |
179 | render() {
180 | return this.renderConsumers(
181 | this.renderProviders(
182 | this.props.children
183 | )
184 | );
185 | }
186 | };
187 | };
188 |
189 | export default createMultiContext;
190 |
--------------------------------------------------------------------------------
/tests/create-multi-context.spec.js:
--------------------------------------------------------------------------------
1 | import Enzyme, { mount, shallow } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | import React from 'react';
4 | import createMultiContext from '../index';
5 |
6 | Enzyme.configure({ adapter: new Adapter() });
7 |
8 | describe('createMultiContext', () => {
9 |
10 | let MultiContext = null;
11 | beforeEach(() => {
12 | MultiContext = createMultiContext({ d: true });
13 | });
14 |
15 | it('should render without crashing', () => {
16 | shallow();
17 | });
18 |
19 | it.skip('should not render contexts without getters/setters', () => {
20 | const multiContext = shallow();
21 | expect(multiContext.find('Context').length).toBe(0);
22 | });
23 |
24 |
25 |
26 | // Set
27 | describe('set', () => {
28 |
29 | it('should accept an object', () => {
30 | expect(() => {
31 | shallow();
32 | }).not.toThrowError();
33 | });
34 | });
35 |
36 |
37 |
38 | // Get
39 | describe('get', () => {
40 |
41 | it('should accept an array and a render prop', () => {
42 | shallow();
43 | expect(() => {
44 | shallow(
45 | null}
47 | get={[ 'a' ]}
48 | />
49 | );
50 | }).not.toThrowError();
51 | });
52 |
53 | it('should require a render prop', () => {
54 | shallow();
55 | expect(() => {
56 | shallow(
57 |
61 | );
62 | }).toThrowError();
63 | expect(() => {
64 | shallow(
65 |
69 | );
70 | }).toThrowError();
71 | expect(() => {
72 | shallow(
73 |
77 | );
78 | }).toThrowError();
79 | });
80 |
81 | it('should execute the render prop', () => {
82 | let value = null;
83 | mount(
84 |
85 |
86 | {a => {
87 | value = a;
88 | return null;
89 | }}
90 |
91 |
92 | );
93 | expect(value).toBe(1);
94 | });
95 | });
96 |
97 |
98 |
99 | // Defaults
100 | describe('default', () => {
101 | it('should accept default parameter', () => {
102 | let value = null;
103 | mount(
104 | {
106 | value = d;
107 | return null;
108 | }}
109 | get={[ 'd' ]}
110 | />
111 | );
112 | expect(value).toBe(true);
113 | });
114 | });
115 |
116 |
117 |
118 | // Set + Get
119 | describe('set + get', () => {
120 |
121 | it('should not get what was just set', () => {
122 | let value = null;
123 | mount(
124 | {
126 | value = d;
127 | return null;
128 | }}
129 | get={[ 'd' ]}
130 | set={{ d: false }}
131 | />
132 | );
133 | expect(value).toBe(true);
134 | });
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/tests/static-with.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createMultiContext from '../index';
3 |
4 | describe('static with', () => {
5 |
6 | let MultiContext = null;
7 | beforeEach(() => {
8 | MultiContext = createMultiContext();
9 | });
10 |
11 | it('should be a static method', () => {
12 | const hoc = MultiContext.with('name');
13 | });
14 |
15 | it('should return a Higher Order Component', () => {
16 | const hoc = MultiContext.with('name');
17 | if (typeof hoc !== 'function') {
18 | throw new Error('Returned a ' + (typeof hoc) + ' (' + hoc + ')');
19 | }
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './src/index.js',
5 | externals: {
6 | 'react': {
7 | amd: 'react',
8 | commonjs: 'react',
9 | commonjs2: 'react',
10 | root: 'React'
11 | }
12 | },
13 | mode: 'production',
14 | module: {
15 | rules: [
16 | {
17 | test: /\.js$/,
18 | use: {
19 | loader: 'babel-loader'
20 | }
21 | }
22 | ]
23 | },
24 | output: {
25 | filename: 'index.js',
26 | library: 'react-multi-context',
27 | libraryTarget: 'umd',
28 | path: path.resolve(__dirname, '.'),
29 | umdNamedDefine: true
30 | },
31 | resolve: {
32 | alias: {
33 | 'react': path.resolve(__dirname, './node_modules/react')
34 | }
35 | }
36 | };
37 |
--------------------------------------------------------------------------------