-
Notifications
You must be signed in to change notification settings - Fork 106
/
Copy pathview.js
160 lines (140 loc) · 5.05 KB
/
view.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { Component, useState, useEffect, useMemo, memo } from 'react';
import {
observe,
unobserve,
raw,
isObservable,
} from '@nx-js/observer-util';
import { hasHooks } from './utils';
export let isInsideFunctionComponent = false;
export let isInsideClassComponentRender = false;
export let isInsideFunctionComponentWithoutHooks = false;
const COMPONENT = Symbol('owner component');
function mapStateToStores(state) {
// find store properties and map them to their none observable raw value
// to do not trigger none static this.setState calls
// from the static getDerivedStateFromProps lifecycle method
const component = state[COMPONENT];
return Object.keys(component)
.map(key => component[key])
.filter(isObservable)
.map(raw);
}
export function view(Comp) {
const isStatelessComp = !(
Comp.prototype && Comp.prototype.isReactComponent
);
let ReactiveComp;
if (isStatelessComp && hasHooks) {
// use a hook based reactive wrapper when we can
ReactiveComp = props => {
// use a dummy setState to update the component
const [, setState] = useState();
// create a memoized reactive wrapper of the original component (render)
// at the very first run of the component function
const render = useMemo(
() =>
observe(Comp, {
scheduler: () => setState({}),
lazy: true,
}),
// Adding the original Comp here is necessary to make React Hot Reload work
// it does not affect behavior otherwise
[Comp],
);
// cleanup the reactive connections after the very last render of the component
useEffect(() => {
return () => unobserve(render);
}, []);
// the isInsideFunctionComponent flag is used to toggle `store` behavior
// based on where it was called from
isInsideFunctionComponent = true;
try {
// run the reactive render instead of the original one
return render(props);
} finally {
isInsideFunctionComponent = false;
}
};
} else {
const BaseComp = isStatelessComp ? Component : Comp;
// a HOC which overwrites render, shouldComponentUpdate and componentWillUnmount
// it decides when to run the new reactive methods and when to proxy to the original methods
class ReactiveClassComp extends BaseComp {
constructor(props, context) {
super(props, context);
this.state = this.state || {};
this.state[COMPONENT] = this;
// create a reactive render for the component
this.render = observe(this.render, {
scheduler: () => this.setState({}),
lazy: true,
});
}
render() {
isInsideClassComponentRender = !isStatelessComp;
isInsideFunctionComponentWithoutHooks = isStatelessComp;
try {
return isStatelessComp
? Comp(this.props, this.context)
: super.render();
} finally {
isInsideClassComponentRender = false;
isInsideFunctionComponentWithoutHooks = false;
}
}
// react should trigger updates on prop changes, while easyState handles store changes
shouldComponentUpdate(nextProps, nextState) {
const { props, state } = this;
// respect the case when the user defines a shouldComponentUpdate
if (super.shouldComponentUpdate) {
return super.shouldComponentUpdate(nextProps, nextState);
}
// return true if it is a reactive render or state changes
if (state !== nextState) {
return true;
}
// the component should update if any of its props shallowly changed value
const keys = Object.keys(props);
const nextKeys = Object.keys(nextProps);
return (
nextKeys.length !== keys.length ||
nextKeys.some(key => props[key] !== nextProps[key])
);
}
// add a custom deriveStoresFromProps lifecyle method
static getDerivedStateFromProps(props, state) {
if (super.deriveStoresFromProps) {
// inject all local stores and let the user mutate them directly
const stores = mapStateToStores(state);
super.deriveStoresFromProps(props, ...stores);
}
// respect user defined getDerivedStateFromProps
if (super.getDerivedStateFromProps) {
return super.getDerivedStateFromProps(props, state);
}
return null;
}
componentWillUnmount() {
// call user defined componentWillUnmount
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
// clean up memory used by Easy State
unobserve(this.render);
}
}
ReactiveComp = ReactiveClassComp;
}
ReactiveComp.displayName = Comp.displayName || Comp.name;
// static props are inherited by class components,
// but have to be copied for function components
if (isStatelessComp) {
Object.keys(Comp).forEach(key => {
ReactiveComp[key] = Comp[key];
});
}
return isStatelessComp && hasHooks
? memo(ReactiveComp)
: ReactiveComp;
}