Skip to content

Commit

Permalink
New API to match context RFC
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mjackson committed Jan 18, 2018
1 parent 08f6bf6 commit 8692c79
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 38 deletions.
49 changes: 11 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ Then, use as you would anything else:

```js
// using ES6 modules
import { Broadcast, Subscriber } from "react-broadcast"
import { createBroadcast } from "react-broadcast"

// using CommonJS modules
var Broadcast = require("react-broadcast").Broadcast
var Subscriber = require("react-broadcast").Subscriber
var createBroadcast = require("react-broadcast").createBroadcast
```

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

## Usage

The following is a totally contrived example, but illustrates the basic functionality we're after:
The following is a contrived example, but illustrates the basic functionality we're after:

```js
import React from "react"
import { Broadcast, Subscriber } from "react-broadcast"
import { createBroadcast } from "react-broadcast"

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

const { Broadcast, Subscriber } = createBroadcast(users[0])

class UpdateBlocker extends React.Component {
shouldComponentUpdate() {
// This is how you indicate to React's reconciler that you don't
Expand All @@ -75,7 +76,7 @@ class UpdateBlocker extends React.Component {

class App extends React.Component {
state = {
currentUser: users[0]
currentUser: Broadcast.initialValue
}

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

render() {
return (
<Broadcast channel="currentUser" value={this.state.currentUser}>
<Broadcast value={this.state.currentUser}>
<UpdateBlocker>
<Subscriber channel="currentUser">
{currentUser => <p>The current user is {currentUser.name}</p>}
</Subscriber>
<Subscriber>{currentUser => <p>The current user is {currentUser.name}</p>}</Subscriber>
</UpdateBlocker>
</Broadcast>
)
}
}
```

By default `<Broadcast value>` values are compared using the `===` (strict equality) operator. To
change this behavior, use `<Broadcast compareValues>` which is a function that takes the `prevValue`
and `nextValue` and compares them. If `compareValues` returns `true`, no re-render will occur.

You may prefer to wrap these components into channel-specific pairs to avoid typos and other
problems with the indirection involved with the channel strings:

```js
// Broadcasts.js
import { Broadcast, Subscriber } from 'react-broadcast'

const CurrentUserChannel = 'currentUser'

export const CurrentUserBroadcast = (props) =>
<Broadcast {...props} channel={CurrentUserChannel} />

export const CurrentUserSubscriber = (props) =>
<Subscriber {...props} channel={CurrentUserChannel} />

// App.js
import { CurrentUserBroadcast, CurrentUserSubscriber } from './Broadcasts'

<CurrentUserBroadcast value={user}/>
<CurrentUserSubscriber>{user => ...}</CurrentUserSubscriber>
```

Enjoy!

## About

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)!
you're interested in learning more about what React can do for your company, please [get in
touch](mailto:hello@reacttraining.com)!
98 changes: 98 additions & 0 deletions modules/__tests__/createBroadcast-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from "react"
import ReactDOM from "react-dom"
import { Simulate } from "react-dom/test-utils"
import createBroadcast from "../createBroadcast"

describe("createBroadcast", () => {
it("creates a Broadcast component", () => {
const { Broadcast } = createBroadcast()
expect(typeof Broadcast).toBe("function")
})

it("creates a Subscriber component", () => {
const { Subscriber } = createBroadcast()
expect(typeof Subscriber).toBe("function")
})
})

describe("A <Subscriber>", () => {
let node
beforeEach(() => {
node = document.createElement("div")
})

it("gets the initial broadcast value on the initial render", done => {
const initialValue = "cupcakes"
const { Subscriber } = createBroadcast(initialValue)

let actualValue

ReactDOM.render(
<Subscriber
children={value => {
actualValue = value
return null
}}
/>,
node,
() => {
expect(actualValue).toBe(initialValue)
done()
}
)
})

it("gets the updated broadcast value as it changes", done => {
const { Broadcast, Subscriber } = createBroadcast("cupcakes")

class Parent extends React.Component {
state = {
value: Broadcast.initialValue
}

render() {
return (
<Broadcast value={this.state.value}>
<button
onClick={() => this.setState({ value: "bubblegum" })}
ref={node => (this.button = node)}
/>
<Child />
</Broadcast>
)
}
}

let childDidRender = false

class Child extends React.Component {
// Make sure we can bypass a sCU=false!
shouldComponentUpdate() {
return false
}

render() {
return (
<Subscriber
children={value => {
if (childDidRender) {
expect(value).toBe("bubblegum")
done()
} else {
expect(value).toBe(Broadcast.initialValue)
}

childDidRender = true

return null
}}
/>
)
}
}

ReactDOM.render(<Parent />, node, function() {
Simulate.click(this.button)
})
})
})
117 changes: 117 additions & 0 deletions modules/createBroadcast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from "react"
import PropTypes from "prop-types"
import invariant from "invariant"

function createBroadcast(initialValue) {
let subscribers = []
let currentValue = initialValue

const publish = value => {
currentValue = value
subscribers.forEach(subscriber => subscriber(currentValue))
}

const subscribe = subscriber => {
subscribers.push(subscriber)
return () => (subscribers = subscribers.filter(item => item !== subscriber))
}

let broadcastInstance = null

/**
* A <Broadcast> is a container for a "value" that its <Subscriber>
* may subscribe to. A <Broadcast> may only be rendered once.
*/
class Broadcast extends React.Component {
/**
* For convenience when setting up a component that tracks this <Broadcast>'s
* value in state.
*
* const { Broadcast, Subscriber } = createBroadcast("value")
*
* class MyComponent {
* state = {
* broadcastValue: Broadcast.initialValue
* }
*
* // ...
*
* render() {
* return <Broadcast value={this.state.broadcastValue}/>
* }
* }
*/
static initialValue = initialValue

componentDidMount() {
invariant(
broadcastInstance == null,
"You cannot render the same <Broadcast> twice! There must be only one source of truth. " +
"Instead of rendering another <Broadcast>, just change the `value` prop of the one " +
"you already rendered."
)

broadcastInstance = this

if (this.props.value !== currentValue) {
// TODO: Publish and warn about the double render
// problem if there are existing subscribers? Or
// just ignore the discrepancy?
}
}

componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value) {
publish(nextProps.value)
}
}

componentWillUnmount() {
if (broadcastInstance === this) {
broadcastInstance = null
}
}

render() {
return this.props.children
}
}

/**
* A <Subscriber> sets state whenever its <Broadcast value> changes
* and calls its render prop with the result.
*/
class Subscriber extends React.Component {
static propTypes = {
children: PropTypes.func
}

state = {
value: currentValue
}

componentDidMount() {
this.unsubscribe = subscribe(value => {
this.setState({ value })
})
}

componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe()
}
}

render() {
const { children } = this.props
return children ? children(this.state.value) : null
}
}

return {
Broadcast,
Subscriber
}
}

export default createBroadcast
2 changes: 2 additions & 0 deletions modules/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export Broadcast from "./Broadcast";
export Subscriber from "./Subscriber";

export createBroadcast from "./createBroadcast";

0 comments on commit 8692c79

Please sign in to comment.