Skip to content

Commit 181847c

Browse files
mjacksonMichael Jackson
authored andcommitted
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 2dc38ea commit 181847c

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:[email protected])!
107+
you're interested in learning more about what React can do for your company, please [get in
108+
touch](mailto:[email protected])!
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)