Skip to content

Commit 59fca73

Browse files
iddanjamiebuilds
authored andcommitted
Added calculateChangedBits() (#11)
* Added calculateChangedBits() * Got rid of __DEV__ * forgot NODE_ENV * Removed babel-preset-fbjs
1 parent 4ddaf7e commit 59fca73

File tree

5 files changed

+213
-12
lines changed

5 files changed

+213
-12
lines changed

examples/theme-example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
"react": "^16.2.0",
1414
"react-dom": "^16.2.0",
1515
"webpack": "^3.10.0"
16-
}
16+
},
17+
"devDependencies": {}
1718
}

src/__tests__/createReactContext.test.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,134 @@ test('with provider', () => {
7272
wrapper.find('button').simulate('click');
7373
expect(wrapper).toMatchSnapshot('with provider - after click');
7474
});
75+
76+
test('can skip consumers with bitmask', () => {
77+
let renders = { Foo: 0, Bar: 0 }
78+
79+
const Context = createReactContext({foo: 0, bar: 0}, (a, b) => {
80+
let result = 0;
81+
if (a.foo !== b.foo) {
82+
result |= 0b01;
83+
}
84+
if (a.bar !== b.bar) {
85+
result |= 0b10;
86+
}
87+
return result;
88+
});
89+
90+
function Provider(props) {
91+
return (
92+
<Context.Provider value={{foo: props.foo, bar: props.bar}}>
93+
{props.children}
94+
</Context.Provider>
95+
);
96+
}
97+
98+
function Foo() {
99+
return (
100+
<Context.Consumer observedBits={0b01}>
101+
{value => {
102+
renders.Foo += 1
103+
return <span prop={'Foo: ' + value.foo} />;
104+
}}
105+
</Context.Consumer>
106+
);
107+
}
108+
109+
function Bar() {
110+
return (
111+
<Context.Consumer observedBits={0b10}>
112+
{value => {
113+
renders.Bar += 1
114+
return <span prop={'Bar: ' + value.bar} />;
115+
}}
116+
</Context.Consumer>
117+
);
118+
}
119+
120+
class Indirection extends React.Component<*> {
121+
shouldComponentUpdate() {
122+
return false;
123+
}
124+
render() {
125+
return this.props.children;
126+
}
127+
}
128+
129+
function App(props) {
130+
return (
131+
<Provider foo={props.foo} bar={props.bar}>
132+
<Indirection>
133+
<Indirection>
134+
<Foo />
135+
</Indirection>
136+
<Indirection>
137+
<Bar />
138+
</Indirection>
139+
</Indirection>
140+
</Provider>
141+
);
142+
}
143+
144+
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)
151+
152+
// 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)
160+
161+
// 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)
169+
170+
// 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+
))
178+
});
179+
180+
test('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
181+
jest.spyOn(global.console, 'error')
182+
183+
const Context = createReactContext(
184+
0,
185+
(a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
186+
);
187+
188+
const wrapper = mount(
189+
<Context.Provider value={1}>
190+
<Context.Consumer>{
191+
value => value
192+
}</Context.Consumer>
193+
</Context.Provider>
194+
);
195+
196+
// Update
197+
wrapper.setProps({ value: 2 });
198+
199+
wrapper.unmount();
200+
201+
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')
204+
}
205+
});

src/index.js

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import React, { Component, type Node } from 'react';
33
import PropTypes from 'prop-types';
44
import gud from 'gud';
5+
import warning from 'fbjs/lib/warning';
6+
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
57

68
type RenderFn<T> = (value: T) => Node;
79

@@ -11,7 +13,8 @@ export type ProviderProps<T> = {
1113
};
1214

1315
export type ConsumerProps<T> = {
14-
children: RenderFn<T> | [RenderFn<T>]
16+
children: RenderFn<T> | [RenderFn<T>],
17+
observedBits?: number
1518
};
1619

1720
export type ConsumerState<T> = {
@@ -41,9 +44,9 @@ function createEventEmitter(value) {
4144
return value;
4245
},
4346

44-
set(newValue) {
47+
set(newValue, changedBits) {
4548
value = newValue;
46-
handlers.forEach(handler => handler(value));
49+
handlers.forEach(handler => handler(value, changedBits));
4750
}
4851
};
4952
}
@@ -52,7 +55,10 @@ function onlyChild(children): any {
5255
return Array.isArray(children) ? children[0] : children;
5356
}
5457

55-
function createReactContext<T>(defaultValue: T): Context<T> {
58+
function createReactContext<T>(
59+
defaultValue: T,
60+
calculateChangedBits: ?(a: T, b: T) => number
61+
): Context<T> {
5662
const contextProp = '__create-react-context-' + gud() + '__';
5763

5864
class Provider extends Component<ProviderProps<T>> {
@@ -70,7 +76,39 @@ function createReactContext<T>(defaultValue: T): Context<T> {
7076

7177
componentWillReceiveProps(nextProps) {
7278
if (this.props.value !== nextProps.value) {
73-
this.emitter.set(nextProps.value);
79+
const oldProps = this.props;
80+
const { value: newValue } = nextProps;
81+
let changedBits: number;
82+
const oldValue = oldProps.value;
83+
// Use Object.is to compare the new context value to the old value.
84+
// Inlined Object.is polyfill.
85+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
86+
if (
87+
(oldValue === newValue &&
88+
(oldValue !== 0 || 1 / oldValue === 1 / newValue)) ||
89+
(oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare
90+
) {
91+
// No change.
92+
changedBits = 0;
93+
} else {
94+
changedBits =
95+
typeof calculateChangedBits === 'function'
96+
? calculateChangedBits(oldValue, newValue)
97+
: MAX_SIGNED_31_BIT_INT;
98+
if (process.env.NODE_ENV !== 'production') {
99+
warning(
100+
(changedBits & MAX_SIGNED_31_BIT_INT) === changedBits,
101+
'calculateChangedBits: Expected the return value to be a ' +
102+
'31-bit integer. Instead received: %s',
103+
changedBits
104+
);
105+
}
106+
changedBits |= 0;
107+
108+
if (changedBits !== 0) {
109+
this.emitter.set(nextProps.value, changedBits);
110+
}
111+
}
74112
}
75113
}
76114

@@ -84,14 +122,29 @@ function createReactContext<T>(defaultValue: T): Context<T> {
84122
[contextProp]: PropTypes.object
85123
};
86124

125+
observedBits: number;
126+
87127
state: ConsumerState<T> = {
88128
value: this.getValue()
89129
};
90130

131+
componentWillReceiveProps(nextProps) {
132+
const { observedBits } = nextProps
133+
this.observedBits = observedBits === undefined || observedBits === null
134+
// Subscribe to all changes by default
135+
? MAX_SIGNED_31_BIT_INT
136+
: observedBits
137+
}
138+
91139
componentDidMount() {
92140
if (this.context[contextProp]) {
93141
this.context[contextProp].on(this.onUpdate);
94142
}
143+
const { observedBits } = this.props
144+
this.observedBits = observedBits === undefined || observedBits === null
145+
// Subscribe to all changes by default
146+
? MAX_SIGNED_31_BIT_INT
147+
: observedBits
95148
}
96149

97150
componentWillUnmount() {
@@ -108,10 +161,11 @@ function createReactContext<T>(defaultValue: T): Context<T> {
108161
}
109162
}
110163

111-
onUpdate = () => {
112-
this.setState({
113-
value: this.getValue()
114-
});
164+
onUpdate = (newValue, changedBits : number) => {
165+
const observedBits: number = this.observedBits | 0;
166+
if ((observedBits & changedBits) !== 0) {
167+
this.setState({ value: this.getValue() })
168+
}
115169
};
116170

117171
render() {

src/index.js.flow

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export type ProviderProps<T> = {
99
};
1010

1111
export type ConsumerProps<T> = {
12-
children: RenderFn<T> | [RenderFn<T>]
12+
children: RenderFn<T> | [RenderFn<T>],
13+
observedProps?: number,
1314
};
1415

1516
export type ConsumerState<T> = {
@@ -25,5 +26,6 @@ export type Context<T> = {
2526
};
2627

2728
declare export default function createReactContext<T>(
28-
defaultValue: T
29+
defaultValue: T,
30+
calculateChangedBits: ?(a : T, b: T) => number
2931
): Context<T>;

src/maxSigned31BitInt.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
11+
// Math.pow(2, 30) - 1
12+
// 0b111111111111111111111111111111
13+
export default 1073741823;

0 commit comments

Comments
 (0)