├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── modules ├── .babelrc ├── .eslintrc ├── Broadcast.js ├── Subscriber.js ├── __tests__ │ ├── .eslintrc │ ├── Subscriber-test.js │ ├── createContext-test.js │ └── integration-test.js ├── createContext.js ├── createDeprecationWarning.js └── index.js ├── package.json ├── scripts ├── build.js └── config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | cjs 4 | esm 5 | umd 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | cache: 4 | directories: 5 | - "$HOME/.cache/yarn" 6 | - node_modules 7 | env: 8 | global: 9 | - secure: pN5iwVeB1hwx0OxzqZ13DzveMkaDmJnw4jPqIYMnnp3G281tAH77SpTqrh7Jg/lrDAyZKNNlvOa6kEK5Ue7Jd5XMCmYvkaAS3gm+uiQV8GNyhD1sfANuzgKAKjB+RGIcWNGe26dRSlKqdbY4gJcqgqqNS2t28kZw9x8P+ExfrfZZTZI4ChCJMJnOA/Nzron+RpJEUCM3nGE0Nzpdrri1E+n4K3WUEM7YGm8qy+O5rxqGwgKYm8frBfE/Rm8Sz+Y4Z0gWdHc92M7lL7JBHHJoDBoGPdyB8I2Cj2Gvzh4qebfJrv+7ar+9nda9VTA9tSaoHn69Mma4rqGQhkD7R3RHDL9ZKy7KuMsDLoIMRCwqKamyzzKhAQjyeZU+CLvE3PoQanIA4oTCjupTWTtfw8f2XCTd8zg8qIzewAg7/qqgUKx2HYyNMsP0xqeYTTVMWChAZ2NhPtDNY8ktt0jjGlL5sltsWzSl2CIx7zFV1W9kkLjdxZublaASA4ouNG4rXSN+eQeHDb/E5WVxNcz9NIDfgGfiGbxb7e+Boz5Cbm+Yz/VZAS35TuU1Q46HiBi2IPjqtbgAG+zq+l01jgDBtF3vnu5iYK88Wyd5Oz+q+LFxpiAxTMhJb3dr65ezG8s/nFNZrJWBRIa78jKTkJSEx8WeBwhAuL1TKi/ROn+famgMhHw= 10 | - NPM_TAG=$([[ "$TRAVIS_TAG" == *-* ]] && echo "next" || echo "latest") 11 | deploy: 12 | provider: npm 13 | email: npm@mjackson.me 14 | api_key: "$NPM_TOKEN" 15 | tag: "$NPM_TAG" 16 | skip_cleanup: true 17 | on: 18 | all_branches: true 19 | condition: '"$TRAVIS_TAG" =~ ^v[0-9]' 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) React Training 2016-2018 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-broadcast [![Travis][build-badge]][build] [![npm package][npm-badge]][npm] 2 | 3 | [build-badge]: https://img.shields.io/travis/ReactTraining/react-broadcast/master.svg?style=flat-square 4 | [build]: https://travis-ci.org/ReactTraining/react-broadcast 5 | [npm-badge]: https://img.shields.io/npm/v/react-broadcast.svg?style=flat-square 6 | [npm]: https://www.npmjs.com/package/react-broadcast 7 | 8 | [`react-broadcast`](https://www.npmjs.com/package/react-broadcast) provides a reliable way for React components to propagate state changes to their descendants deep in the component hierarchy, bypassing intermediaries who `return false` from [`shouldComponentUpdate`](https://reactjs.org/docs/react-component.html#shouldcomponentupdate). 9 | 10 | It was originally built to solve issues that arose from using [`react-router`](https://www.npmjs.com/package/react-router) together with [`react-redux`](https://www.npmjs.com/package/react-redux). The router needed a safe way to communicate state changes to ``s deep in the component hierarchy, but `react-redux` relies on `shouldComponentUpdate` for performance. `react-broadcast` allows the router to work seamlessly with Redux and any other component that uses `shouldComponentUpdate`. 11 | 12 | **Please note:** As with anything that uses [context](https://reactjs.org/docs/context.html), this library is experimental. It may cease working in some future version of React. For now, it's a practical workaround for the router. If we discover some better way to do things in the future, rest assured we'll do our best to share what we learn. 13 | 14 | ## Installation 15 | 16 | $ npm install --save react-broadcast 17 | 18 | Then, use as you would anything else: 19 | 20 | ```js 21 | // using ES6 modules 22 | import { createContext } from "react-broadcast"; 23 | 24 | // using CommonJS modules 25 | var createContext = require("react-broadcast").createContext; 26 | ``` 27 | 28 | The UMD build is also available on [unpkg](https://unpkg.com): 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | You can find the library on `window.ReactBroadcast`. 35 | 36 | ## Usage 37 | 38 | The following is a contrived example, but illustrates the basic functionality we're after: 39 | 40 | ```js 41 | import React from "react"; 42 | import { createContext } from "react-broadcast"; 43 | 44 | const users = [{ name: "Michael Jackson" }, { name: "Ryan Florence" }]; 45 | 46 | const { Provider, Consumer } = createContext(users[0]); 47 | 48 | class UpdateBlocker extends React.Component { 49 | shouldComponentUpdate() { 50 | // This is how you indicate to React's reconciler that you don't 51 | // need to be updated. It's a great way to boost performance when 52 | // you're sure (based on your props and state) that your render 53 | // output will not change, but it makes it difficult for libraries 54 | // to communicate changes down the hierarchy that you don't really 55 | // know anything about. 56 | return false; 57 | } 58 | 59 | render() { 60 | return this.props.children; 61 | } 62 | } 63 | 64 | class App extends React.Component { 65 | state = { 66 | currentUser: Provider.defaultValue 67 | }; 68 | 69 | componentDidMount() { 70 | // Randomly change the current user every 2 seconds. 71 | setInterval(() => { 72 | const index = Math.floor(Math.random() * users.length); 73 | this.setState({ currentUser: users[index] }); 74 | }, 2000); 75 | } 76 | 77 | render() { 78 | return ( 79 | 80 | 81 | 82 | {currentUser =>

The current user is {currentUser.name}

} 83 |
84 |
85 |
86 | ); 87 | } 88 | } 89 | ``` 90 | 91 | Enjoy! 92 | 93 | ## About 94 | 95 | react-broadcast is developed and maintained by [React Training](https://reacttraining.com). If you're interested in learning more about what React can do for your company, please [get in touch](mailto:hello@reacttraining.com)! 96 | -------------------------------------------------------------------------------- /modules/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "loose": true }], "stage-1", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /modules/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "import", 5 | "react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:import/errors" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /modules/Broadcast.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import invariant from "invariant"; 4 | import createDeprecationWarning from "./createDeprecationWarning"; 5 | 6 | const deprecationWarning = createDeprecationWarning(); 7 | 8 | function createBroadcast(initialValue) { 9 | let currentValue = initialValue; 10 | let subscribers = []; 11 | 12 | const getValue = () => currentValue; 13 | 14 | const publish = state => { 15 | currentValue = state; 16 | subscribers.forEach(s => s(currentValue)); 17 | }; 18 | 19 | const subscribe = subscriber => { 20 | subscribers.push(subscriber); 21 | 22 | return () => { 23 | subscribers = subscribers.filter(s => s !== subscriber); 24 | }; 25 | }; 26 | 27 | return { 28 | getValue, 29 | publish, 30 | subscribe 31 | }; 32 | } 33 | 34 | /** 35 | * A provides a generic way for descendants to "subscribe" 36 | * to some value that changes over time, bypassing any intermediate 37 | * shouldComponentUpdate's in the hierarchy. It puts all subscription 38 | * functions on context.broadcasts, keyed by "channel". 39 | * 40 | * To use it, a subscriber must opt-in to context.broadcasts. See the 41 | * component for a reference implementation. 42 | */ 43 | class Broadcast extends React.Component { 44 | static propTypes = { 45 | channel: PropTypes.string.isRequired, 46 | children: PropTypes.node.isRequired, 47 | compareValues: PropTypes.func, 48 | value: PropTypes.any 49 | }; 50 | 51 | static defaultProps = { 52 | compareValues: (prevValue, nextValue) => prevValue === nextValue 53 | }; 54 | 55 | static contextTypes = { 56 | broadcasts: PropTypes.object 57 | }; 58 | 59 | static childContextTypes = { 60 | broadcasts: PropTypes.object.isRequired 61 | }; 62 | 63 | broadcast = createBroadcast(this.props.value); 64 | 65 | getChildContext() { 66 | return { 67 | broadcasts: { 68 | ...this.context.broadcasts, 69 | [this.props.channel]: this.broadcast 70 | } 71 | }; 72 | } 73 | 74 | componentWillMount() { 75 | deprecationWarning( 76 | " is deprecated and will be removed in the next major release. " + 77 | "Please use createContext instead. See https://goo.gl/QAF37J for more info." 78 | ); 79 | } 80 | 81 | componentWillReceiveProps(nextProps) { 82 | invariant( 83 | this.props.channel === nextProps.channel, 84 | "You cannot change " 85 | ); 86 | 87 | if (!this.props.compareValues(this.props.value, nextProps.value)) { 88 | this.broadcast.publish(nextProps.value); 89 | } 90 | } 91 | 92 | render() { 93 | return React.Children.only(this.props.children); 94 | } 95 | } 96 | 97 | export default Broadcast; 98 | -------------------------------------------------------------------------------- /modules/Subscriber.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import invariant from "invariant"; 4 | import createDeprecationWarning from "./createDeprecationWarning"; 5 | 6 | const deprecationWarning = createDeprecationWarning(); 7 | 8 | /** 9 | * A pulls the value for a channel off of context.broadcasts 10 | * and passes it to its children function. 11 | */ 12 | class Subscriber extends React.Component { 13 | static propTypes = { 14 | channel: PropTypes.string.isRequired, 15 | children: PropTypes.func, 16 | quiet: PropTypes.bool 17 | }; 18 | 19 | static defaultProps = { 20 | quiet: false 21 | }; 22 | 23 | static contextTypes = { 24 | broadcasts: PropTypes.object 25 | }; 26 | 27 | state = { 28 | value: undefined 29 | }; 30 | 31 | getBroadcast() { 32 | const broadcasts = this.context.broadcasts || {}; 33 | const broadcast = broadcasts[this.props.channel]; 34 | 35 | invariant( 36 | this.props.quiet || broadcast, 37 | ' must be rendered in the context of a ', 38 | this.props.channel, 39 | this.props.channel 40 | ); 41 | 42 | return broadcast; 43 | } 44 | 45 | componentWillMount() { 46 | deprecationWarning( 47 | " is deprecated and will be removed in the next major release. " + 48 | "Please use createContext instead. See https://goo.gl/QAF37J for more info." 49 | ); 50 | 51 | const broadcast = this.getBroadcast(); 52 | 53 | if (broadcast) { 54 | this.setState({ 55 | value: broadcast.getValue() 56 | }); 57 | } 58 | } 59 | 60 | componentDidMount() { 61 | const broadcast = this.getBroadcast(); 62 | 63 | if (broadcast) { 64 | this.unsubscribe = broadcast.subscribe(value => { 65 | this.setState({ value }); 66 | }); 67 | } 68 | } 69 | 70 | componentWillUnmount() { 71 | if (this.unsubscribe) this.unsubscribe(); 72 | } 73 | 74 | render() { 75 | const { children } = this.props; 76 | return children ? children(this.state.value) : null; 77 | } 78 | } 79 | 80 | export default Subscriber; 81 | -------------------------------------------------------------------------------- /modules/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "jest" 4 | ], 5 | "env": { 6 | "jest/globals": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/__tests__/Subscriber-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMServer from "react-dom/server"; 3 | import Subscriber from "../Subscriber"; 4 | 5 | describe("A ", () => { 6 | it("throws an invariant when it is not rendered in the context of a ", () => { 7 | expect(() => { 8 | ReactDOMServer.renderToStaticMarkup(); 9 | }).toThrow(); 10 | }); 11 | 12 | describe("with quiet=true", () => { 13 | it("does not throw when it is not rendered in the context of a ", () => { 14 | expect(() => { 15 | ReactDOMServer.renderToStaticMarkup( 16 | 17 | ); 18 | }).not.toThrow(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /modules/__tests__/createContext-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Simulate } from "react-dom/test-utils"; 4 | import createContext from "../createContext"; 5 | 6 | describe("createContext", () => { 7 | it("creates a Provider component", () => { 8 | const { Provider } = createContext(); 9 | expect(typeof Provider).toBe("function"); 10 | }); 11 | 12 | it("creates a Consumer component", () => { 13 | const { Consumer } = createContext(); 14 | expect(typeof Consumer).toBe("function"); 15 | }); 16 | }); 17 | 18 | describe("A ", () => { 19 | it("knows its default value", () => { 20 | const defaultValue = "bubblegum"; 21 | const { Provider } = createContext(defaultValue); 22 | expect(defaultValue).toEqual(Provider.defaultValue); 23 | }); 24 | }); 25 | 26 | describe("A ", () => { 27 | let node; 28 | beforeEach(() => { 29 | node = document.createElement("div"); 30 | }); 31 | 32 | it("gets the initial broadcast value on the initial render", done => { 33 | const defaultValue = "cupcakes"; 34 | const { Provider, Consumer } = createContext(defaultValue); 35 | 36 | let actualValue; 37 | 38 | ReactDOM.render( 39 | 40 | { 42 | actualValue = value; 43 | return null; 44 | }} 45 | /> 46 | , 47 | node, 48 | () => { 49 | expect(actualValue).toBe(defaultValue); 50 | done(); 51 | } 52 | ); 53 | }); 54 | 55 | it("gets the updated broadcast value as it changes", done => { 56 | const { Provider, Consumer } = createContext("cupcakes"); 57 | 58 | class Parent extends React.Component { 59 | state = { 60 | value: Provider.defaultValue 61 | }; 62 | 63 | render() { 64 | return ( 65 | 66 |