Skip to content

Commit 42c66bb

Browse files
committed
New API to match context RFC
This is an API overhaul to more closely match the API currently being proposed in reactjs/rfcs#2 The main goals of this work are: - Conform more closely to the upcoming context API, to make it easier for people to migrate off react-broadcast when that API eventually lands - Remove reliance on context entirely, since eventually it'll be gone - Remove ambiguity around "channel"s The new API looks like: const { Broadcast, Subscriber } = createBroadcast(initialValue) <Broadcast value="anything-you-want-here"> <Subscriber children={value => ( // ... )} /> </Broadcast> Instead of providing pre-built <Broadcast> and <Subscriber> components, we provide a createBroadcast function that may be used to create them. See the README for further usage instructions.
1 parent 50f1568 commit 42c66bb

File tree

4 files changed

+228
-38
lines changed

4 files changed

+228
-38
lines changed

README.md

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ Then, use as you would anything else:
3232

3333
```js
3434
// using ES6 modules
35-
import { Broadcast, Subscriber } from "react-broadcast"
35+
import { createBroadcast } from "react-broadcast"
3636

3737
// using CommonJS modules
38-
var Broadcast = require("react-broadcast").Broadcast
39-
var Subscriber = require("react-broadcast").Subscriber
38+
var createBroadcast = require("react-broadcast").createBroadcast
4039
```
4140

4241
The UMD build is also available on [unpkg](https://unpkg.com):
@@ -49,14 +48,16 @@ You can find the library on `window.ReactBroadcast`.
4948

5049
## Usage
5150

52-
The following is a totally contrived example, but illustrates the basic functionality we're after:
51+
The following is a contrived example, but illustrates the basic functionality we're after:
5352

5453
```js
5554
import React from "react"
56-
import { Broadcast, Subscriber } from "react-broadcast"
55+
import { createBroadcast } from "react-broadcast"
5756

5857
const users = [{ name: "Michael Jackson" }, { name: "Ryan Florence" }]
5958

59+
const { Broadcast, Subscriber } = createBroadcast(users[0])
60+
6061
class UpdateBlocker extends React.Component {
6162
shouldComponentUpdate() {
6263
// This is how you indicate to React's reconciler that you don't
@@ -75,7 +76,7 @@ class UpdateBlocker extends React.Component {
7576

7677
class App extends React.Component {
7778
state = {
78-
currentUser: users[0]
79+
currentUser: Broadcast.initialValue
7980
}
8081

8182
componentDidMount() {
@@ -88,48 +89,20 @@ class App extends React.Component {
8889

8990
render() {
9091
return (
91-
<Broadcast channel="currentUser" value={this.state.currentUser}>
92+
<Broadcast value={this.state.currentUser}>
9293
<UpdateBlocker>
93-
<Subscriber channel="currentUser">
94-
{currentUser => <p>The current user is {currentUser.name}</p>}
95-
</Subscriber>
94+
<Subscriber>{currentUser => <p>The current user is {currentUser.name}</p>}</Subscriber>
9695
</UpdateBlocker>
9796
</Broadcast>
9897
)
9998
}
10099
}
101100
```
102101

103-
By default `<Broadcast value>` values are compared using the `===` (strict equality) operator. To
104-
change this behavior, use `<Broadcast compareValues>` which is a function that takes the `prevValue`
105-
and `nextValue` and compares them. If `compareValues` returns `true`, no re-render will occur.
106-
107-
You may prefer to wrap these components into channel-specific pairs to avoid typos and other
108-
problems with the indirection involved with the channel strings:
109-
110-
```js
111-
// Broadcasts.js
112-
import { Broadcast, Subscriber } from 'react-broadcast'
113-
114-
const CurrentUserChannel = 'currentUser'
115-
116-
export const CurrentUserBroadcast = (props) =>
117-
<Broadcast {...props} channel={CurrentUserChannel} />
118-
119-
export const CurrentUserSubscriber = (props) =>
120-
<Subscriber {...props} channel={CurrentUserChannel} />
121-
122-
// App.js
123-
import { CurrentUserBroadcast, CurrentUserSubscriber } from './Broadcasts'
124-
125-
<CurrentUserBroadcast value={user}/>
126-
<CurrentUserSubscriber>{user => ...}</CurrentUserSubscriber>
127-
```
128-
129102
Enjoy!
130103

131104
## About
132105

133106
react-broadcast is developed and maintained by [React Training](https://reacttraining.com). If
134-
you're interested in learning more about what React can do for your company, please
135-
[get in touch](mailto:hello@reacttraining.com)!
107+
you're interested in learning more about what React can do for your company, please [get in
108+
touch](mailto:hello@reacttraining.com)!
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React from "react"
2+
import ReactDOM from "react-dom"
3+
import { Simulate } from "react-dom/test-utils"
4+
import createBroadcast from "../createBroadcast"
5+
6+
describe("createBroadcast", () => {
7+
it("creates a Broadcast component", () => {
8+
const { Broadcast } = createBroadcast()
9+
expect(typeof Broadcast).toBe("function")
10+
})
11+
12+
it("creates a Subscriber component", () => {
13+
const { Subscriber } = createBroadcast()
14+
expect(typeof Subscriber).toBe("function")
15+
})
16+
})
17+
18+
describe("A <Subscriber>", () => {
19+
let node
20+
beforeEach(() => {
21+
node = document.createElement("div")
22+
})
23+
24+
it("gets the initial broadcast value on the initial render", done => {
25+
const initialValue = "cupcakes"
26+
const { Subscriber } = createBroadcast(initialValue)
27+
28+
let actualValue
29+
30+
ReactDOM.render(
31+
<Subscriber
32+
children={value => {
33+
actualValue = value
34+
return null
35+
}}
36+
/>,
37+
node,
38+
() => {
39+
expect(actualValue).toBe(initialValue)
40+
done()
41+
}
42+
)
43+
})
44+
45+
it("gets the updated broadcast value as it changes", done => {
46+
const { Broadcast, Subscriber } = createBroadcast("cupcakes")
47+
48+
class Parent extends React.Component {
49+
state = {
50+
value: Broadcast.initialValue
51+
}
52+
53+
render() {
54+
return (
55+
<Broadcast value={this.state.value}>
56+
<button
57+
onClick={() => this.setState({ value: "bubblegum" })}
58+
ref={node => (this.button = node)}
59+
/>
60+
<Child />
61+
</Broadcast>
62+
)
63+
}
64+
}
65+
66+
let childDidRender = false
67+
68+
class Child extends React.Component {
69+
// Make sure we can bypass a sCU=false!
70+
shouldComponentUpdate() {
71+
return false
72+
}
73+
74+
render() {
75+
return (
76+
<Subscriber
77+
children={value => {
78+
if (childDidRender) {
79+
expect(value).toBe("bubblegum")
80+
done()
81+
} else {
82+
expect(value).toBe(Broadcast.initialValue)
83+
}
84+
85+
childDidRender = true
86+
87+
return null
88+
}}
89+
/>
90+
)
91+
}
92+
}
93+
94+
ReactDOM.render(<Parent />, node, function() {
95+
Simulate.click(this.button)
96+
})
97+
})
98+
})

modules/createBroadcast.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from "react"
2+
import PropTypes from "prop-types"
3+
import invariant from "invariant"
4+
5+
function createBroadcast(initialValue) {
6+
let subscribers = []
7+
let currentValue = initialValue
8+
9+
const publish = value => {
10+
currentValue = value
11+
subscribers.forEach(subscriber => subscriber(currentValue))
12+
}
13+
14+
const subscribe = subscriber => {
15+
subscribers.push(subscriber)
16+
return () => (subscribers = subscribers.filter(item => item !== subscriber))
17+
}
18+
19+
let broadcastInstance = null
20+
21+
/**
22+
* A <Broadcast> is a container for a "value" that its <Subscriber>
23+
* may subscribe to. A <Broadcast> may only be rendered once.
24+
*/
25+
class Broadcast extends React.Component {
26+
/**
27+
* For convenience when setting up a component that tracks this <Broadcast>'s
28+
* value in state.
29+
*
30+
* const { Broadcast, Subscriber } = createBroadcast("value")
31+
*
32+
* class MyComponent {
33+
* state = {
34+
* broadcastValue: Broadcast.initialValue
35+
* }
36+
*
37+
* // ...
38+
*
39+
* render() {
40+
* return <Broadcast value={this.state.broadcastValue}/>
41+
* }
42+
* }
43+
*/
44+
static initialValue = initialValue
45+
46+
componentDidMount() {
47+
invariant(
48+
broadcastInstance == null,
49+
"You cannot render the same <Broadcast> twice! There must be only one source of truth. " +
50+
"Instead of rendering another <Broadcast>, just change the `value` prop of the one " +
51+
"you already rendered."
52+
)
53+
54+
broadcastInstance = this
55+
56+
if (this.props.value !== currentValue) {
57+
// TODO: Publish and warn about the double render
58+
// problem if there are existing subscribers? Or
59+
// just ignore the discrepancy?
60+
}
61+
}
62+
63+
componentWillReceiveProps(nextProps) {
64+
if (this.props.value !== nextProps.value) {
65+
publish(nextProps.value)
66+
}
67+
}
68+
69+
componentWillUnmount() {
70+
if (broadcastInstance === this) {
71+
broadcastInstance = null
72+
}
73+
}
74+
75+
render() {
76+
return this.props.children
77+
}
78+
}
79+
80+
/**
81+
* A <Subscriber> sets state whenever its <Broadcast value> changes
82+
* and calls its render prop with the result.
83+
*/
84+
class Subscriber extends React.Component {
85+
static propTypes = {
86+
children: PropTypes.func
87+
}
88+
89+
state = {
90+
value: currentValue
91+
}
92+
93+
componentDidMount() {
94+
this.unsubscribe = subscribe(value => {
95+
this.setState({ value })
96+
})
97+
}
98+
99+
componentWillUnmount() {
100+
if (this.unsubscribe) {
101+
this.unsubscribe()
102+
}
103+
}
104+
105+
render() {
106+
const { children } = this.props
107+
return children ? children(this.state.value) : null
108+
}
109+
}
110+
111+
return {
112+
Broadcast,
113+
Subscriber
114+
}
115+
}
116+
117+
export default createBroadcast

modules/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export Broadcast from "./Broadcast";
22
export Subscriber from "./Subscriber";
3+
4+
export createBroadcast from "./createBroadcast";

0 commit comments

Comments
 (0)