Skip to content

Commit be456d3

Browse files
committed
add MemoizedRender
1 parent 1349adb commit be456d3

File tree

10 files changed

+233
-19
lines changed

10 files changed

+233
-19
lines changed

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ circle.yml
55
stories/
66
_tests/
77
src/
8+
assets/
89

910
.DS_Store
1011
.size-limit

.size-limit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
33
path: "dist/index.js",
4-
limit: "6.7 KB"
4+
limit: "7.5 KB"
55
}
66
]

README.md

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
# react-memoize 🤯 🧠 🧙
2-
[![CircleCI status](https://img.shields.io/circleci/project/github/theKashey/react-memoize/master.svg?style=flat-square)](https://circleci.com/gh/theKashey/react-focus-lock/tree/master)
1+
<div align="center">
2+
<h1>react-memoize 🤯 🧠</h1>
3+
<br/>
4+
<img src="./assets/logo.png" alt="memoize" height="187" align="center">
5+
<br/>
6+
[![CircleCI status](https://img.shields.io/circleci/project/github/theKashey/react-memoize/master.svg?style=flat-square)](https://circleci.com/gh/theKashey/react-focus-lock/tree/master)
7+
</div>
38

9+
7kb library to change the world. It is not fast, but it is MUCH faster that VDOM tree comparison you will face in case of render trashing.
10+
Uses [memoize-state](https://github.com/theKashey/memoize-state) underneath, providing the same magic for `get` as [immer](https://github.com/mweststrate/immer) provided to `set`.
411

5-
Component memoization for React! Based on [Dan Abramov's tweet](https://twitter.com/dan_abramov/status/965378278461755392) :)
12+
__Just write code as you want. It it will be properly memoized__.
13+
14+
This is declarative component memoization for React! Based on [Dan Abramov's tweet](https://twitter.com/dan_abramov/status/965378278461755392)
615
Could change the way you did `componentWillReceiveProps`, could replace `getDerivedStateFromProps`, could make things better.
716

817
> IE11+, React 15 and React 16.3 compatible.
918
1019
[![NPM](https://nodei.co/npm/react-memoize.png?downloads=true&stars=true)](https://nodei.co/npm/react-memoize/)
1120

21+
- [Memoize](#Memoize) - to create declarative memoized selection.
22+
- [MemoizedFlow](#MemoizedFlow) - to create declarative memoized flow.
23+
- [MemoizeContext](#MemoizeContext) - to create memoized selector from context(or any Consumer).
24+
- [MemoizedRender](#MemoizedRender) - to create a render, memoized by a value provided.
25+
26+
Memoize, MemoizedFlow, MemoizeContext accepts one or more functions to select or transform
27+
incoming data, and provide result to a function-as-child.
28+
29+
MemoizedRender is memoizing the function-as-child itself.
1230
### Memoize
1331

1432
```js
@@ -17,9 +35,9 @@ Could change the way you did `componentWillReceiveProps`, could replace `getDeri
1735
<Memoize
1836
prop1 = "theKey"
1937
state = {this.state}
20-
21-
compute={({prop1, state}) => heavyComputation(state[prop1])}
22-
pure
38+
// values from above will be provided to compute function
39+
compute={({prop1, state}) => heavyComputation(state[prop1])} // Memoize tracks WHAT you are doing
40+
pure // Memoize will be a pure component itself
2341
>
2442
{ result => <Display>{result}</Display>}
2543
</Memoize>
@@ -42,7 +60,7 @@ import {MemoizeContext} from 'react-memoize';
4260
</MemoizeContext>
4361
</Context.Provider>
4462
```
45-
`consumer` could be any "context"-compatible Component - React.context, create-react-context or unstated.
63+
`consumer` could be any "context"-compatible Component - React.context, create-react-context, unstated, react-copy-write.
4664
All the additional props will be passed down to consumer.
4765

4866
It is better to explain using example.
@@ -108,6 +126,42 @@ own result over it. Until the last step will be reached, and output will be prov
108126
109127
Each step is memoized, as usual, and will always reuse value from the steps before.
110128
129+
# MemoizedRender
130+
MemoizedRender is mostly usable with Context API
131+
```js
132+
import {MemoizedRender} from 'react-memoize';
133+
134+
<Context.Provider value={{prop1: 1, prop2: 2, prop3: 3}}>
135+
<MemoizedRender consumer={Context.Consumer}>
136+
{values => <Render {...select(values)} />}
137+
</MemoizeContext>
138+
</Context.Provider>
139+
```
140+
Or, the better example (from [react-copy-write](https://github.com/aweary/react-copy-write#consuming-state))
141+
```js
142+
const UserAvatar = ({ id }) => (
143+
<MemoizedRender consumer={State.Consumer}>
144+
{state => (
145+
<div className="avatar">
146+
<img src={state.users[id].avatar.src} />
147+
</div>
148+
)}
149+
</MemoizedRender>
150+
);
151+
```
152+
While `react-copy-write` declares that _ The problem with this is that whenever any value in state changes, UserAvatar will be re-rendered, even though it's only using a single property from a single, nested object._
153+
This example will work, as long MemoizedRender will track used keys, and perform update only when necessary.
154+
155+
It is also possible to provide `value` as a prop
156+
```js
157+
<MemoizedRender value={originalValue}>
158+
{values => <Render {...select(values)} />}
159+
</MemoizeContext>
160+
```
161+
162+
MemoizedRender __memoizes "render" as a whole__. This is __absolute pure component__. Be carefull. Might be not 100% compatible with async rendering
163+
if you pass values you were provided down the tree, as long __async accessed keys are not tracked__.
164+
Thus - MemoizedRender may not react to changes in them.
111165
112166
## About
113167

_tests/memoize.spec.js

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import createContext from 'create-react-context';
33
import Enzyme, {mount} from 'enzyme';
44
import Adapter from 'enzyme-adapter-react-16';
5-
import Memoize, {MemoizeContext, MemoizedFlow} from '../src';
5+
import Memoize, {MemoizeContext, MemoizedFlow, MemoizedRender} from '../src';
66

77
Enzyme.configure({adapter: new Adapter()});
88

@@ -174,6 +174,67 @@ describe('React memoize', () => {
174174

175175
});
176176

177+
describe('render', () => {
178+
it('render with value', () => {
179+
180+
const Component = jest.fn().mockImplementation(({value}) => <div>comp - {value}</div>)
181+
182+
const wrapper = mount(
183+
<MemoizedRender value={{key1: 1, key2: 2}}>
184+
{values => <Component value={values.key1}/>}
185+
</MemoizedRender>
186+
);
187+
188+
expect(Component).toHaveBeenCalledTimes(1);
189+
expect(wrapper.html()).toBe("<div>comp - 1</div>");
190+
191+
wrapper.setProps({value: {key1: 1, key2: 2}});
192+
expect(Component).toHaveBeenCalledTimes(1);
193+
194+
wrapper.setProps({value: {key1: 1, key2: 3}});
195+
expect(Component).toHaveBeenCalledTimes(1);
196+
197+
wrapper.setProps({value: {key1: 1, key2: 3, newKey: 1}});
198+
expect(Component).toHaveBeenCalledTimes(1);
199+
200+
wrapper.setProps({value: {key1: 2, key2: 3, newKey: 1}});
201+
expect(Component).toHaveBeenCalledTimes(2);
202+
expect(wrapper.html()).toBe("<div>comp - 2</div>");
203+
204+
})
205+
206+
it('render with context', () => {
207+
208+
const Component = jest.fn().mockImplementation(({value}) => <div>comp - {value}</div>)
209+
const Context = createContext('test');
210+
211+
const wrapper = mount(
212+
<Context.Provider value={{key1: 1, key2: 2}}>
213+
<MemoizedRender consumer={Context.Consumer}>
214+
{values => <Component value={values.key1}/>}
215+
</MemoizedRender>
216+
</Context.Provider>
217+
);
218+
219+
expect(Component).toHaveBeenCalledTimes(1);
220+
expect(wrapper.html()).toBe("<div>comp - 1</div>");
221+
222+
wrapper.setProps({value: {key1: 1, key2: 2}});
223+
expect(Component).toHaveBeenCalledTimes(1);
224+
225+
wrapper.setProps({value: {key1: 1, key2: 3}});
226+
expect(Component).toHaveBeenCalledTimes(1);
227+
228+
wrapper.setProps({value: {key1: 1, key2: 3, newKey: 1}});
229+
expect(Component).toHaveBeenCalledTimes(1);
230+
231+
wrapper.setProps({value: {key1: 2, key2: 3, newKey: 1}});
232+
expect(Component).toHaveBeenCalledTimes(2);
233+
expect(wrapper.html()).toBe("<div>comp - 2</div>");
234+
235+
})
236+
});
237+
177238
describe('flow', () => {
178239
it('the flow', () => {
179240
let lastState = 0;
@@ -214,7 +275,7 @@ describe('React memoize', () => {
214275
expect(spy2).toHaveBeenCalledTimes(1);
215276
expect(spy3).toHaveBeenCalledTimes(1);
216277

217-
wrapper.setProps({input:{...input, page:1}});
278+
wrapper.setProps({input: {...input, page: 1}});
218279
expect(wrapper.text()).toBe('5');
219280
expect(lastState).toEqual({list: [2, 3], page: 1, sortOrder: 1, filter: null});
220281

@@ -232,23 +293,23 @@ describe('React memoize', () => {
232293
expect(spy3).toHaveBeenCalledTimes(2);
233294
}
234295

235-
wrapper.setProps({input:{...input, filter: x => x%2}});
296+
wrapper.setProps({input: {...input, filter: x => x % 2}});
236297
expect(wrapper.text()).toBe('4');
237298
expect(lastState).toEqual({list: [1, 3], page: 0, sortOrder: 1, filter: null});
238299

239300
expect(spy1).toHaveBeenCalledTimes(2);
240301
expect(spy2).toHaveBeenCalledTimes(2);
241302
expect(spy3).toHaveBeenCalledTimes(3);
242303

243-
wrapper.setProps({input:{...input}});
304+
wrapper.setProps({input: {...input}});
244305
expect(wrapper.text()).toBe('3');
245306
expect(lastState).toEqual({list: [1, 2], page: 0, sortOrder: 1, filter: null});
246307

247308
expect(spy1).toHaveBeenCalledTimes(3);
248309
expect(spy2).toHaveBeenCalledTimes(3);
249310
expect(spy3).toHaveBeenCalledTimes(4);
250311

251-
wrapper.setProps({input:{...input, sortOrder: -1 }});
312+
wrapper.setProps({input: {...input, sortOrder: -1}});
252313
expect(wrapper.text()).toBe('11');
253314
expect(lastState).toEqual({list: [6, 5], page: 0, sortOrder: -1, filter: null});
254315

@@ -257,7 +318,7 @@ describe('React memoize', () => {
257318
expect(spy3).toHaveBeenCalledTimes(5);
258319

259320
// try reset flow
260-
wrapper.setProps({input:{...input, sortOrder: -1 }, flow:[]});
321+
wrapper.setProps({input: {...input, sortOrder: -1}, flow: []});
261322
expect(wrapper.text()).toBe('11');
262323
expect(lastState).toEqual({list: [6, 5], page: 0, sortOrder: -1, filter: null});
263324

assets/logo.png

185 KB
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"homepage": "https://github.com/theKashey/react-memoize#readme",
6868
"dependencies": {
6969
"memoize-one": "^3.0.1",
70-
"memoize-state": "^1.3.3",
70+
"memoize-state": "^1.4.1",
7171
"prop-types": "15.5.10",
7272
"react-lifecycles-compat": "^1.1.0"
7373
}

src/MemoizedRender.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, { PureComponent } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import memoizeState from 'memoize-state';
5+
6+
/* eslint-disable no-underscore-dangle, react/no-multi-comp */
7+
8+
class MemoizedRenderIndirect extends PureComponent {
9+
static propTypes = {
10+
value: PropTypes.any.isRequired,
11+
};
12+
13+
render() {
14+
return this.props.value;
15+
}
16+
}
17+
18+
const deproxifyShouldDive = (data, key, a) => key === '_owner' && a.$$typeof && a._store;
19+
20+
export class MemoizedRender extends PureComponent {
21+
static propTypes = {
22+
children: PropTypes.func.isRequired,
23+
value: PropTypes.object,
24+
consumer: PropTypes.any,
25+
};
26+
27+
static defaultProps = {
28+
value: null,
29+
consumer: null,
30+
};
31+
32+
state = {
33+
children: memoizeState(this.props.children, { flags: { deproxifyShouldDive } }),
34+
};
35+
36+
renderProp = value => (
37+
<MemoizedRenderIndirect value={this.state.children(value)} fn={this.state.children} />
38+
);
39+
40+
render() {
41+
const { value, consumer: Consumer } = this.props;
42+
43+
if (value) {
44+
return this.renderProp(value);
45+
}
46+
if (this.props.consumer) {
47+
return <Consumer>{this.renderProp}</Consumer>;
48+
}
49+
return null;
50+
}
51+
}

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import MemoizeDefault, { MemoizeOne, MemoizeState } from './Memoizer';
22
import { MemoizeContext } from './Context';
3+
import { MemoizedRender } from './MemoizedRender';
34
import { MemoizedFlow } from './Waterflow';
45

56
export {
67
MemoizeOne,
78
MemoizeState,
89
MemoizeContext,
10+
MemoizedRender,
911
MemoizedFlow,
1012
};
1113

stories/index.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {Component} from 'react';
22
import {storiesOf} from '@storybook/react';
33

4-
import Memoize from '../src';
4+
import Memoize, {MemoizedRender} from '../src';
55

66

77
const heavyFunction = (list, count) => {
@@ -77,5 +77,50 @@ class Test1 extends Component {
7777
}
7878
}
7979

80+
81+
class TestMemoizedRender extends Component {
82+
state = {
83+
count: 1,
84+
generation: 0,
85+
list: new Array(50).fill(1).map((x, index) => index)
86+
};
87+
88+
componentDidMount() {
89+
setInterval(() => this.setState({generation: this.state.generation + 1}), 1000);
90+
}
91+
92+
decC = () => this.setState(({count}) => ({count: count - 1}));
93+
incC = () => this.setState(({count}) => ({count: count + 1}));
94+
95+
render() {
96+
return (
97+
<div>
98+
Valuable:
99+
<button onClick={this.decC}>-</button>{this.state.count}
100+
<button onClick={this.incC}>+</button>
101+
<span> gen : {this.state.generation}</span>
102+
103+
<MemoizedRender
104+
value={this.state}
105+
>{state => {
106+
const {list, time} = heavyFunction(state.list, state.count);
107+
return (
108+
<div>
109+
render #{this.state.generation}, updated {Math.round((Date.now() - time) / 1000)} seconds ago
110+
<ul>
111+
{list.map(x => <li key={x}>{x}</li>)}
112+
</ul>
113+
</div>
114+
)
115+
}
116+
}
117+
</MemoizedRender>
118+
</div>
119+
);
120+
}
121+
}
122+
123+
80124
storiesOf('Memoize', module)
81125
.add('example', () => <Test1/>)
126+
.add('MemoizedRender', () => <TestMemoizedRender/>)

yarn.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5125,9 +5125,9 @@ memoize-one@^3.0.1:
51255125
version "3.0.1"
51265126
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.0.1.tgz#7b599850bb41be8beed305f4eefd963c8cea9a0a"
51275127

5128-
memoize-state@^1.3.3:
5129-
version "1.3.3"
5130-
resolved "https://registry.yarnpkg.com/memoize-state/-/memoize-state-1.3.3.tgz#061d5054f4d85de011c4258c3a20eab80531341a"
5128+
memoize-state@^1.4.1:
5129+
version "1.4.1"
5130+
resolved "https://registry.yarnpkg.com/memoize-state/-/memoize-state-1.4.1.tgz#f2e84f56d2da2c61dfb54f0f333a80bd404970f9"
51315131
dependencies:
51325132
function-double "^1.0.1"
51335133
proxyequal "^1.5.0"

0 commit comments

Comments
 (0)