Skip to content

Commit 38d0497

Browse files
committed
Fixes and use React.createContext when available
1 parent a50a8a7 commit 38d0497

File tree

5 files changed

+230
-227
lines changed

5 files changed

+230
-227
lines changed

src/__tests__/createReactContext.test.js

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @flow
22
import 'raf/polyfill';
3-
import createReactContext, { type Context } from '../';
3+
import createReactContext, { type Context } from '../implementation';
44
import React, { type Node } from 'react';
55
import Enzyme, { mount } from 'enzyme';
66
import Adapter from 'enzyme-adapter-react-16';
@@ -74,9 +74,9 @@ test('with provider', () => {
7474
});
7575

7676
test('can skip consumers with bitmask', () => {
77-
let renders = { Foo: 0, Bar: 0 }
77+
let renders = { Foo: 0, Bar: 0 };
7878

79-
const Context = createReactContext({foo: 0, bar: 0}, (a, b) => {
79+
const Context = createReactContext({ foo: 0, bar: 0 }, (a, b) => {
8080
let result = 0;
8181
if (a.foo !== b.foo) {
8282
result |= 0b01;
@@ -89,7 +89,7 @@ test('can skip consumers with bitmask', () => {
8989

9090
function Provider(props) {
9191
return (
92-
<Context.Provider value={{foo: props.foo, bar: props.bar}}>
92+
<Context.Provider value={{ foo: props.foo, bar: props.bar }}>
9393
{props.children}
9494
</Context.Provider>
9595
);
@@ -99,7 +99,7 @@ test('can skip consumers with bitmask', () => {
9999
return (
100100
<Context.Consumer observedBits={0b01}>
101101
{value => {
102-
renders.Foo += 1
102+
renders.Foo += 1;
103103
return <span prop={'Foo: ' + value.foo} />;
104104
}}
105105
</Context.Consumer>
@@ -110,7 +110,7 @@ test('can skip consumers with bitmask', () => {
110110
return (
111111
<Context.Consumer observedBits={0b10}>
112112
{value => {
113-
renders.Bar += 1
113+
renders.Bar += 1;
114114
return <span prop={'Bar: ' + value.bar} />;
115115
}}
116116
</Context.Consumer>
@@ -142,54 +142,46 @@ test('can skip consumers with bitmask', () => {
142142
}
143143

144144
const wrapper = mount(<App foo={1} bar={1} />);
145-
expect(renders.Foo).toBe(1)
146-
expect(renders.Bar).toBe(1)
147-
expect(wrapper.contains(
148-
<span prop='Foo: 1' />,
149-
<span prop='Bar: 1' />,
150-
)).toBe(true)
145+
expect(renders.Foo).toBe(1);
146+
expect(renders.Bar).toBe(1);
147+
expect(wrapper.contains(<span prop="Foo: 1" />, <span prop="Bar: 1" />)).toBe(
148+
true
149+
);
151150

152151
// Update only foo
153-
wrapper.setProps({ foo: 2, bar: 1 })
154-
expect(renders.Foo).toBe(2)
155-
expect(renders.Bar).toBe(1)
156-
expect(wrapper.contains(
157-
<span prop='Foo: 2' />,
158-
<span prop='Bar: 1' />,
159-
)).toBe(true)
152+
wrapper.setProps({ foo: 2, bar: 1 });
153+
expect(renders.Foo).toBe(2);
154+
expect(renders.Bar).toBe(1);
155+
expect(wrapper.contains(<span prop="Foo: 2" />, <span prop="Bar: 1" />)).toBe(
156+
true
157+
);
160158

161159
// Update only bar
162-
wrapper.setProps({ bar: 2, foo: 2 })
163-
expect(renders.Foo).toBe(2)
164-
expect(renders.Bar).toBe(2)
165-
expect(wrapper.contains(
166-
<span prop='Foo: 2' />,
167-
<span prop='Bar: 2' />,
168-
)).toBe(true)
160+
wrapper.setProps({ bar: 2, foo: 2 });
161+
expect(renders.Foo).toBe(2);
162+
expect(renders.Bar).toBe(2);
163+
expect(wrapper.contains(<span prop="Foo: 2" />, <span prop="Bar: 2" />)).toBe(
164+
true
165+
);
169166

170167
// Update both
171-
wrapper.setProps({ bar: 3, foo: 3 })
172-
expect(renders.Foo).toBe(3)
173-
expect(renders.Bar).toBe(3)
174-
expect(wrapper.contains(
175-
<span prop='Foo: 3' />,
176-
<span prop='Bar: 3' />,
177-
))
168+
wrapper.setProps({ bar: 3, foo: 3 });
169+
expect(renders.Foo).toBe(3);
170+
expect(renders.Bar).toBe(3);
171+
expect(wrapper.contains(<span prop="Foo: 3" />, <span prop="Bar: 3" />));
178172
});
179173

180174
test('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
181-
jest.spyOn(global.console, 'error')
175+
jest.spyOn(global.console, 'error');
182176

183177
const Context = createReactContext(
184178
0,
185-
(a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
179+
(a, b) => Math.pow(2, 32) - 1 // Return 32 bit int
186180
);
187181

188182
const wrapper = mount(
189183
<Context.Provider value={1}>
190-
<Context.Consumer>{
191-
value => value
192-
}</Context.Consumer>
184+
<Context.Consumer>{value => value}</Context.Consumer>
193185
</Context.Provider>
194186
);
195187

@@ -199,7 +191,9 @@ test('warns if calculateChangedBits returns larger than a 31-bit integer', () =>
199191
wrapper.unmount();
200192

201193
if (process.env.NODE_ENV !== 'production') {
202-
expect(console.error).toHaveBeenCalledTimes(1)
203-
expect(console.error).lastCalledWith('Warning: calculateChangedBits: Expected the return value to be a 31-bit integer. Instead received: 4294967295')
194+
expect(console.error).toHaveBeenCalledTimes(1);
195+
expect(console.error).lastCalledWith(
196+
'Warning: calculateChangedBits: Expected the return value to be a 31-bit integer. Instead received: 4294967295'
197+
);
204198
}
205199
});

src/implementation.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// @flow
2+
import React, { Component, type Node } from 'react';
3+
import PropTypes from 'prop-types';
4+
import gud from 'gud';
5+
import warning from 'fbjs/lib/warning';
6+
7+
const MAX_SIGNED_31_BIT_INT = 1073741823;
8+
9+
// Inlined Object.is polyfill.
10+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
11+
function objectIs(x, y) {
12+
if (x === y) {
13+
return x !== 0 || 1 / x === 1 / (y: any);
14+
} else {
15+
return x !== x && y !== y;
16+
}
17+
}
18+
19+
type RenderFn<T> = (value: T) => Node;
20+
21+
export type ProviderProps<T> = {
22+
value: T,
23+
children?: Node
24+
};
25+
26+
export type ConsumerProps<T> = {
27+
children: RenderFn<T> | [RenderFn<T>],
28+
observedBits?: number
29+
};
30+
31+
export type ConsumerState<T> = {
32+
value: T
33+
};
34+
35+
export type Provider<T> = Component<ProviderProps<T>>;
36+
export type Consumer<T> = Component<ConsumerProps<T>, ConsumerState<T>>;
37+
38+
export type Context<T> = {
39+
Provider: Class<Provider<T>>,
40+
Consumer: Class<Consumer<T>>
41+
};
42+
43+
function createEventEmitter(value) {
44+
let handlers = [];
45+
return {
46+
on(handler) {
47+
handlers.push(handler);
48+
},
49+
50+
off(handler) {
51+
handlers = handlers.filter(h => h !== handler);
52+
},
53+
54+
get() {
55+
return value;
56+
},
57+
58+
set(newValue, changedBits) {
59+
value = newValue;
60+
handlers.forEach(handler => handler(value, changedBits));
61+
}
62+
};
63+
}
64+
65+
function onlyChild(children): any {
66+
return Array.isArray(children) ? children[0] : children;
67+
}
68+
69+
function createReactContext<T>(
70+
defaultValue: T,
71+
calculateChangedBits: ?(a: T, b: T) => number
72+
): Context<T> {
73+
const contextProp = '__create-react-context-' + gud() + '__';
74+
75+
class Provider extends Component<ProviderProps<T>> {
76+
emitter = createEventEmitter(this.props.value);
77+
78+
static childContextTypes = {
79+
[contextProp]: PropTypes.object.isRequired
80+
};
81+
82+
getChildContext() {
83+
return {
84+
[contextProp]: this.emitter
85+
};
86+
}
87+
88+
componentWillReceiveProps(nextProps) {
89+
if (this.props.value !== nextProps.value) {
90+
let oldValue = this.props.value;
91+
let newValue = nextProps.value;
92+
let changedBits: number;
93+
94+
if (objectIs(oldValue, newValue)) {
95+
changedBits = 0; // No change
96+
} else {
97+
changedBits =
98+
typeof calculateChangedBits === 'function'
99+
? calculateChangedBits(oldValue, newValue)
100+
: MAX_SIGNED_31_BIT_INT;
101+
if (process.env.NODE_ENV !== 'production') {
102+
warning(
103+
(changedBits & MAX_SIGNED_31_BIT_INT) === changedBits,
104+
'calculateChangedBits: Expected the return value to be a ' +
105+
'31-bit integer. Instead received: %s',
106+
changedBits
107+
);
108+
}
109+
110+
changedBits |= 0;
111+
112+
if (changedBits !== 0) {
113+
this.emitter.set(nextProps.value, changedBits);
114+
}
115+
}
116+
}
117+
}
118+
119+
render() {
120+
return this.props.children;
121+
}
122+
}
123+
124+
class Consumer extends Component<ConsumerProps<T>, ConsumerState<T>> {
125+
static contextTypes = {
126+
[contextProp]: PropTypes.object
127+
};
128+
129+
observedBits: number;
130+
131+
state: ConsumerState<T> = {
132+
value: this.getValue()
133+
};
134+
135+
componentWillReceiveProps(nextProps) {
136+
let { observedBits } = nextProps;
137+
this.observedBits =
138+
observedBits === undefined || observedBits === null
139+
? MAX_SIGNED_31_BIT_INT // Subscribe to all changes by default
140+
: observedBits;
141+
}
142+
143+
componentDidMount() {
144+
if (this.context[contextProp]) {
145+
this.context[contextProp].on(this.onUpdate);
146+
}
147+
let { observedBits } = this.props;
148+
this.observedBits =
149+
observedBits === undefined || observedBits === null
150+
? MAX_SIGNED_31_BIT_INT // Subscribe to all changes by default
151+
: observedBits;
152+
}
153+
154+
componentWillUnmount() {
155+
if (this.context[contextProp]) {
156+
this.context[contextProp].off(this.onUpdate);
157+
}
158+
}
159+
160+
getValue(): T {
161+
if (this.context[contextProp]) {
162+
return this.context[contextProp].get();
163+
} else {
164+
return defaultValue;
165+
}
166+
}
167+
168+
onUpdate = (newValue, changedBits: number) => {
169+
const observedBits: number = this.observedBits | 0;
170+
if ((observedBits & changedBits) !== 0) {
171+
this.setState({ value: this.getValue() });
172+
}
173+
};
174+
175+
render() {
176+
return onlyChild(this.props.children)(this.state.value);
177+
}
178+
}
179+
180+
return {
181+
Provider,
182+
Consumer
183+
};
184+
}
185+
186+
export default React.createContext || createReactContext;

src/index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as React from 'react';
22

3-
export default function createReactContext<T>(defaultValue: T): Context<T>;
3+
export default function createReactContext<T>(
4+
defaultValue: T,
5+
calculateChangedBits?: (prev: T, next: T) => number
6+
): Context<T>;
47

58
type RenderFn<T> = (value: T) => React.ReactNode;
69

@@ -16,4 +19,5 @@ export type ProviderProps<T> = {
1619

1720
export type ConsumerProps<T> = {
1821
children: RenderFn<T> | [RenderFn<T>];
22+
observedBits?: number;
1923
};

0 commit comments

Comments
 (0)