@cleanweb/react 1.0.6 → 1.0.8
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +304 -23
- package/build/base/merged-state.d.ts +1 -2
- package/build/base/merged-state.js +4 -7
- package/build/base/state.d.ts +4 -12
- package/build/base/state.js +52 -42
- package/build/classy/class.d.ts +11 -9
- package/build/classy/class.js +20 -7
- package/build/classy/instance.js +6 -9
- package/build/classy/logic.js +13 -1
- package/build/index.js +17 -7
- package/package.json +2 -1
package/README.md
CHANGED
@@ -1,30 +1,311 @@
|
|
1
|
-
# Structured React Function Components
|
2
|
-
This package provides a suite of tools for writing cleaner React function components. It is particularly useful for larger components with lots of state variables and multiple closure functions that need to access those variables. Below is a brief summary of the exported members. A more robust documentation is in the works.
|
1
|
+
# Structured & Cleaner React Function Components
|
3
2
|
|
4
|
-
##
|
5
|
-
|
3
|
+
## Quick Start
|
4
|
+
This package provides a suite of tools for writing cleaner React function components. It is particularly useful for larger components with lots of state variables and multiple closure functions that need to access those variables. The most likely use cases will use one of the two main exported members.
|
5
|
+
|
6
|
+
### Extracting and Structuring Component Logic
|
7
|
+
The `useLogic` allows you to write your component's logic outside the function component's body, and helps you keep them all better organized. It also provides a much cleaner API for working with multiple state variables. Here's what a function component looks like with the `useLogic` hook.
|
8
|
+
|
9
|
+
**Before**
|
10
|
+
```jsx
|
11
|
+
const Button = (props) => {
|
12
|
+
const { param } = props;
|
13
|
+
|
14
|
+
const [state1, setState1] = useState();
|
15
|
+
const [state2, setState2] = useState();
|
16
|
+
const [label, setLabel] = useState('Click me');
|
17
|
+
const [submitted, setSubmitted] = useState(false);
|
18
|
+
|
19
|
+
const memoizedValue = useMemo(() => getValue(param), [param]);
|
20
|
+
|
21
|
+
const subscribeToExternalDataSource = useCallback(() => {
|
22
|
+
externalDataSource.subscribe((data) => {
|
23
|
+
setLabel(data.label);
|
24
|
+
});
|
25
|
+
}, [setLabel]);
|
26
|
+
|
27
|
+
useEffect(subscribeToExternalDataSource, []);
|
28
|
+
|
29
|
+
const submit = useCallback(() => {
|
30
|
+
sendData(state1, state2);
|
31
|
+
setSubmitted(true);
|
32
|
+
}, [state1]); // Notice how `state2` above could easily be stale by the time the callback runs.
|
33
|
+
|
34
|
+
return <>
|
35
|
+
<p>{memoizedValue}</p>
|
36
|
+
<button onClick={submit}>
|
37
|
+
{label}
|
38
|
+
</button>
|
39
|
+
</>;
|
40
|
+
}
|
41
|
+
```
|
42
|
+
|
43
|
+
**After**
|
6
44
|
```jsx
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
45
|
+
class ButtonLogic {
|
46
|
+
static getInitialState = () => {
|
47
|
+
return {
|
48
|
+
state1: undefined,
|
49
|
+
state2: null,
|
50
|
+
label: 'Click me',
|
51
|
+
submitted: false,
|
52
|
+
};
|
53
|
+
}
|
54
|
+
|
55
|
+
submit = () => {
|
56
|
+
const { state1, state2 } = this.state;
|
57
|
+
sendData(state1, state2);
|
58
|
+
this.state.submitted = true;
|
59
|
+
}
|
60
|
+
|
61
|
+
subscribeToExternalDataSource = () => {
|
62
|
+
externalDataSource.subscribe((data) => {
|
63
|
+
this.state.label = data.label;
|
64
|
+
});
|
65
|
+
}
|
66
|
+
|
67
|
+
useHooks = () => {
|
68
|
+
const { param } = this.props;
|
69
|
+
|
70
|
+
useEffect(this.subscribeToExternalDataSource, []);
|
71
|
+
const memoizedValue = useMemo(() => getValue(param), [param]);
|
72
|
+
|
73
|
+
return { memoizedValue };
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
// Button Template
|
78
|
+
const Button = (props) => {
|
79
|
+
const { state, hooks, ...methods } = useLogic(ButtonLogic, props);
|
80
|
+
|
81
|
+
return <>
|
82
|
+
<p>{hooks.memoizedValue}</p>
|
83
|
+
<button onClick={methods.submit}>
|
84
|
+
{state.label}
|
85
|
+
</button>
|
86
|
+
</>;
|
87
|
+
}
|
16
88
|
```
|
17
89
|
|
18
|
-
|
19
|
-
To be finished
|
90
|
+
The `useLogic` hook combines the functionality of two base hooks which can also be used directly. They are [`useCleanState`](https://cleanjsweb.github.io/neat-react/clean-state/index) and [`useMethods`](https://cleanjsweb.github.io/neat-react/methods/index). `useCleanState` can be used independently if you only want a cleaner state management API. `useMethods` is designed to be used together with `useCleanState`, but rather than calling both individually, you may find it more convenient to use `useLogic`, which combines both and also adds additional functionality.
|
20
91
|
|
21
|
-
|
22
|
-
To be finished
|
92
|
+
> It is possible to have multiple calls to `useLogic` in the same component. This allows your function component template to consume state and logic from multiple sources, or it can simply be used to group distinct pieces of related logic into separate classes.
|
23
93
|
|
24
|
-
|
25
|
-
|
26
|
-
|
94
|
+
For a fuller discussion of how `useLogic` works, start at the [clean-state documentation](https://cleanjsweb.github.io/neat-react/clean-state/index).
|
95
|
+
For an API reference, see the [API reference](https://cleanjsweb.github.io/neat-react/logic/api).
|
96
|
+
|
97
|
+
|
98
|
+
### Working With Lifecycle, and Migrating From a React.Component Class to a Function Component
|
99
|
+
In addition to having cleaner and more structured component logic, you can also simplify the process of working with your component's lifecycle with the final two exported members. The `useInstance` hook builds on the functionality of `useLogic` and adds lifecyle methods to the class. This means the class can now be thought of as truly representing a single instance of a React component. The `ClassComponent` class extends this to its fullest by allowing you to write the function component itself as a method within the class, and removing the need to explicitly call `useInstance`.
|
100
|
+
|
101
|
+
**Before**
|
102
|
+
```jsx
|
103
|
+
const Button = (props) => {
|
104
|
+
const [state1, setState1] = useState(props.defaultValue);
|
105
|
+
const [state2, setState2] = useState();
|
106
|
+
const [label, setLabel] = useState('Click me');
|
107
|
+
const [submitted, setSubmitted] = useState(false);
|
108
|
+
const [store, updateStore] = useGlobalStore();
|
109
|
+
|
110
|
+
// Required to run once *before* the component mounts.
|
111
|
+
const memoizedValue = useMemo(() => getValue(), []);
|
112
|
+
|
113
|
+
// Required to run once *after* the component mounts.
|
114
|
+
useEffect(() => {
|
115
|
+
const unsubscribe = externalDataSource.subscribe((data) => {
|
116
|
+
setLabel(data.label);
|
117
|
+
});
|
118
|
+
|
119
|
+
const onWindowResize = () => {};
|
120
|
+
|
121
|
+
window.addEventListener('resize', onWindowResize);
|
122
|
+
|
123
|
+
return () => {
|
124
|
+
unsubscribe();
|
125
|
+
window.removeEventListener('resize', onWindowResize);
|
126
|
+
};
|
127
|
+
}, []);
|
128
|
+
|
129
|
+
// Run *after* every render.
|
130
|
+
useEffect(() => {
|
131
|
+
doSomething();
|
132
|
+
return () => {};
|
133
|
+
})
|
134
|
+
|
135
|
+
const submit = useCallback(() => {
|
136
|
+
sendData(state1, state2);
|
137
|
+
setSubmitted(true);
|
138
|
+
}, [state1]);
|
139
|
+
|
140
|
+
// Run before every render.
|
141
|
+
const text = `${label}, please.`;
|
142
|
+
|
143
|
+
return <>
|
144
|
+
{memoizedValue ? memoizedValue.map((copy) => (
|
145
|
+
<p>{copy}</p>
|
146
|
+
)) : null}
|
147
|
+
<button onClick={submit}>
|
148
|
+
{text}
|
149
|
+
</button>
|
150
|
+
</>;
|
151
|
+
}
|
152
|
+
|
153
|
+
export default Button;
|
154
|
+
```
|
155
|
+
|
156
|
+
**After**
|
157
|
+
```jsx
|
158
|
+
class Button extends ClassComponent {
|
159
|
+
static getInitialState = (props) => {
|
160
|
+
return {
|
161
|
+
state1: props.defaultValue,
|
162
|
+
state2: null,
|
163
|
+
label: 'Click me',
|
164
|
+
submitted: false,
|
165
|
+
};
|
166
|
+
}
|
27
167
|
|
28
|
-
|
29
|
-
|
30
|
-
|
168
|
+
useHooks = () => {
|
169
|
+
const [store, updateStore] = useGlobalStore();
|
170
|
+
return { store, updateStore };
|
171
|
+
}
|
172
|
+
|
173
|
+
/***************************
|
174
|
+
* New Lifecycle Methods *
|
175
|
+
***************************/
|
176
|
+
|
177
|
+
beforeMount = () => {
|
178
|
+
this.memoizedValue = getValue();
|
179
|
+
}
|
180
|
+
|
181
|
+
// Run after the component is mounted.
|
182
|
+
onMount = () => {
|
183
|
+
const unsubscribe = this.subscribeToExternalDataSource();
|
184
|
+
window.addEventListener('resize', this.onWindowResize);
|
185
|
+
|
186
|
+
// Return cleanup callback.
|
187
|
+
return () => {
|
188
|
+
unsubscribe();
|
189
|
+
window.removeEventListener('resize', this.onWindowResize);
|
190
|
+
};
|
191
|
+
}
|
192
|
+
|
193
|
+
beforeRender = () => {
|
194
|
+
this.text = `${label}, please.`;
|
195
|
+
}
|
196
|
+
|
197
|
+
// Run after every render.
|
198
|
+
onRender = () => {
|
199
|
+
doSomething();
|
200
|
+
|
201
|
+
// Return cleanup callback.
|
202
|
+
return () => {};
|
203
|
+
}
|
204
|
+
|
205
|
+
cleanUp = () => {
|
206
|
+
// Run some non-mount-related cleanup when the component dismounts.
|
207
|
+
// onMount (and onRender) returns its own cleanup function.
|
208
|
+
}
|
209
|
+
|
210
|
+
/***************************
|
211
|
+
* [End] Lifecycle Methods *
|
212
|
+
***************************/
|
213
|
+
|
214
|
+
submit = () => {
|
215
|
+
// Methods are guaranteed to have access to the most recent state values,
|
216
|
+
// without any delicate hoops to jump through.
|
217
|
+
const { state1, state2 } = this.state;
|
218
|
+
|
219
|
+
sendData(state1, state2);
|
220
|
+
|
221
|
+
// CleanState uses JavaScript's getters and setters, allowing you to assign state values directly.
|
222
|
+
// The effect is the same as if you called the setter function, which is available through `state.put.submitted(true)`.
|
223
|
+
this.state.submitted = true;
|
224
|
+
}
|
225
|
+
|
226
|
+
onWindowResize = () => {
|
227
|
+
;
|
228
|
+
}
|
229
|
+
|
230
|
+
subscribeToExternalDataSource = () => {
|
231
|
+
const unsubscribe = externalDataSource.subscribe((data) => {
|
232
|
+
this.state.label = data.label;
|
233
|
+
});
|
234
|
+
|
235
|
+
return unsubscribe;
|
236
|
+
}
|
237
|
+
|
238
|
+
/** You can also separate out discreet chunks of your UI template. */
|
239
|
+
Paragraphs = () => {
|
240
|
+
if (!this.memoizedValue) return null;
|
241
|
+
|
242
|
+
return this.memoizedValue.map((content, index) => (
|
243
|
+
<p key={index}>
|
244
|
+
{content || this.state.label}
|
245
|
+
</p>
|
246
|
+
));
|
247
|
+
}
|
248
|
+
|
249
|
+
/** Button Template */
|
250
|
+
Render = () => {
|
251
|
+
const { Paragraphs, submit, state } = this;
|
252
|
+
|
253
|
+
return <>
|
254
|
+
<Paragraphs />
|
255
|
+
|
256
|
+
{/* You can access the setter functions returned from useState through the state.put object. */}
|
257
|
+
{/* This is more convenient than the assignment approach if you need to pass a setter as a callback. */}
|
258
|
+
{/* Use state.putMany to set multiple values at once. It works just like setState in React.Component classes. */}
|
259
|
+
{/* e.g state.inputValue = 'foo', or state.put.inputValue('foo'), or state.putMany({ inputValue: 'foo' }) */}
|
260
|
+
<CustomInput setValue={state.put.inputValue}>
|
261
|
+
|
262
|
+
<button onClick={submit}>
|
263
|
+
{this.text}
|
264
|
+
</button>
|
265
|
+
</>;
|
266
|
+
}
|
267
|
+
}
|
268
|
+
|
269
|
+
// Call the static method FC() to get a function component that you can render like any other function component.
|
270
|
+
export default Button.FC();
|
271
|
+
```
|
272
|
+
|
273
|
+
> If you would like to keep the actual function component separate and call `useInstance` directly, see the [`useInstance` docs](https://cleanjsweb.github.io/neat-react/instance/index) for more details and examples.
|
274
|
+
|
275
|
+
At its core, any component you write with `ClassComponent` is still just a React function component, with some supporting logic around it. This has the added advantage of making it significantly easier to migrate class components written with `React.Component` to the newer hooks-based function components, while still maintaining the overall structure of a class component, and the advantages that the class component approach provided.
|
276
|
+
|
277
|
+
For a fuller discussion of how this works, start at the [`useInstance` documentation](https://cleanjsweb.github.io/neat-react/instance/index).
|
278
|
+
For more details on the lifecycle methods and other API reference, see the [`ClassComponent` API docs](https://cleanjsweb.github.io/neat-react/class-component/api).
|
279
|
+
|
280
|
+
### The `<Use>` Component
|
281
|
+
If you only want to use hooks in your `React.Component` class without having to refactor anything, use the [`Use` component](https://cleanjsweb.github.io/neat-react/class-component/index#the-use-component).
|
282
|
+
|
283
|
+
```jsx
|
284
|
+
class Button extends React.Component {
|
285
|
+
handleGlobalStore = ([store, updateStore]) => {
|
286
|
+
this.setState({ userId: store.userId });
|
287
|
+
this.store = store;
|
288
|
+
this.updateStore = updateStore;
|
289
|
+
}
|
290
|
+
|
291
|
+
UseHooks = () => {
|
292
|
+
return <>
|
293
|
+
<Use hook={useGlobalStore}
|
294
|
+
onUpdate={handleGlobalStore}
|
295
|
+
argumentsList={[]}
|
296
|
+
key="useGlobalStore"
|
297
|
+
/>
|
298
|
+
</>;
|
299
|
+
}
|
300
|
+
|
301
|
+
render() {
|
302
|
+
const { UseHooks } = this;
|
303
|
+
|
304
|
+
return <>
|
305
|
+
<UseHooks />
|
306
|
+
|
307
|
+
<button>Click me</button>
|
308
|
+
</>;
|
309
|
+
}
|
310
|
+
}
|
311
|
+
```
|
@@ -1,12 +1,11 @@
|
|
1
1
|
declare class MergedState<TState extends object> {
|
2
|
-
static
|
2
|
+
static useRefresh<TState extends object>(this: MergedState<TState>): void;
|
3
3
|
reservedKeys: string[];
|
4
4
|
valueKeys: string[];
|
5
5
|
private _initialValues_;
|
6
6
|
private _values_;
|
7
7
|
private setState;
|
8
8
|
private _setters_;
|
9
|
-
private useRetrieveState;
|
10
9
|
get put(): { [Key in keyof TState]: (value: TState[Key]) => void; };
|
11
10
|
get initialState(): TState;
|
12
11
|
constructor(initialState: TState);
|
@@ -19,10 +19,6 @@ var MergedState = /** @class */ (function () {
|
|
19
19
|
this._initialValues_ = {};
|
20
20
|
this._values_ = {};
|
21
21
|
this._setters_ = {};
|
22
|
-
this.useRetrieveState = function () {
|
23
|
-
var _a;
|
24
|
-
_a = (0, react_1.useState)(_this.initialState), _this._values_ = _a[0], _this.setState = _a[1];
|
25
|
-
};
|
26
22
|
this.putMany = function (newValues) {
|
27
23
|
_this.setState(__assign(__assign({}, _this._values_), newValues));
|
28
24
|
};
|
@@ -52,8 +48,9 @@ var MergedState = /** @class */ (function () {
|
|
52
48
|
});
|
53
49
|
});
|
54
50
|
}
|
55
|
-
MergedState.
|
56
|
-
|
51
|
+
MergedState.useRefresh = function () {
|
52
|
+
var _a;
|
53
|
+
_a = (0, react_1.useState)(this.initialState), this._values_ = _a[0], this.setState = _a[1];
|
57
54
|
};
|
58
55
|
Object.defineProperty(MergedState.prototype, "put", {
|
59
56
|
get: function () {
|
@@ -73,7 +70,7 @@ var MergedState = /** @class */ (function () {
|
|
73
70
|
}());
|
74
71
|
var useMergedState = function (initialState) {
|
75
72
|
var cleanState = (0, react_1.useMemo)(function () { return new MergedState(initialState); }, []);
|
76
|
-
MergedState.
|
73
|
+
MergedState.useRefresh.call(cleanState);
|
77
74
|
return cleanState;
|
78
75
|
};
|
79
76
|
exports.useMergedState = useMergedState;
|
package/build/base/state.d.ts
CHANGED
@@ -1,24 +1,16 @@
|
|
1
|
-
type TUseStateArray<TState extends object> = [
|
2
|
-
val: TState[keyof TState],
|
3
|
-
setter: (val: TState[keyof TState]) => void
|
4
|
-
];
|
5
|
-
type TUseStateResponses<TState extends object> = {
|
6
|
-
[Key in keyof TState]: TUseStateArray<TState>;
|
7
|
-
};
|
8
1
|
declare class CleanStateBase<TState extends object> {
|
9
|
-
static update: ICleanStateClass['update'];
|
10
2
|
reservedKeys: string[];
|
11
3
|
valueKeys: string[];
|
12
4
|
private _values_;
|
5
|
+
private _initialValues_;
|
13
6
|
private _setters_;
|
7
|
+
constructor(initialState: TState);
|
8
|
+
static update: <TState_1 extends object>(this: CleanStateBase<TState_1>) => void;
|
14
9
|
get put(): { [Key in keyof TState]: (value: TState[Key]) => void; };
|
15
|
-
|
10
|
+
get initialState(): TState;
|
16
11
|
putMany: (newValues: Partial<TState>) => void;
|
17
12
|
}
|
18
13
|
type TCleanStateInstance<TState extends object> = TState & CleanStateBase<TState>;
|
19
|
-
interface ICleanStateClass {
|
20
|
-
update: <TState extends object>(this: CleanStateBase<TState>, stateAndSetters: TUseStateResponses<TState>) => void;
|
21
|
-
}
|
22
14
|
export type TCleanState<TState extends object> = TCleanStateInstance<TState>;
|
23
15
|
type Func = (...params: any[]) => any;
|
24
16
|
type UseCleanState = <TState extends object, TProps extends object = object>(_initialState: ((props?: TProps) => TState) | TState, // TStateObjOrFactory,
|
package/build/base/state.js
CHANGED
@@ -14,9 +14,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.useMountState = exports.useCleanState = void 0;
|
15
15
|
var react_1 = require("react");
|
16
16
|
var CleanStateBase = /** @class */ (function () {
|
17
|
-
function CleanStateBase() {
|
17
|
+
function CleanStateBase(initialState) {
|
18
18
|
var _this = this;
|
19
|
-
this.valueKeys = [];
|
20
19
|
this._values_ = {};
|
21
20
|
this._setters_ = {};
|
22
21
|
this.putMany = function (newValues) {
|
@@ -26,22 +25,20 @@ var CleanStateBase = /** @class */ (function () {
|
|
26
25
|
});
|
27
26
|
};
|
28
27
|
this.reservedKeys = Object.keys(this);
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
28
|
+
/**
|
29
|
+
* The keys from the initial state object.
|
30
|
+
* By capturing and storing the value once, we ensure that any potential changes to the object,
|
31
|
+
* or irregularities in the order of keys returned by Object.keys,
|
32
|
+
* will not affect the order of subsequent useState calls.
|
33
|
+
* Only keys provided on the initial call will be recognized,
|
34
|
+
* since CleanState is instantiated only once with useMemo,
|
35
|
+
* and they will always be processed in a consistent order during rerenders.
|
36
|
+
*/
|
37
|
+
this.valueKeys = Object.keys(initialState);
|
38
|
+
this._initialValues_ = __assign({}, initialState);
|
39
|
+
this.valueKeys.forEach(function (key) {
|
41
40
|
if (_this.reservedKeys.includes(key))
|
42
41
|
throw new Error("The name \"".concat(key, "\" is reserved by CleanState and cannot be used to index state variables. Please use a different key."));
|
43
|
-
_this.valueKeys.push(key);
|
44
|
-
_this._values_[key] = responseFromUseState[0], _this._setters_[key] = responseFromUseState[1];
|
45
42
|
var self = _this;
|
46
43
|
Object.defineProperty(_this, key, {
|
47
44
|
get: function () {
|
@@ -53,39 +50,52 @@ var CleanStateBase = /** @class */ (function () {
|
|
53
50
|
enumerable: true,
|
54
51
|
});
|
55
52
|
});
|
53
|
+
}
|
54
|
+
Object.defineProperty(CleanStateBase.prototype, "put", {
|
55
|
+
get: function () {
|
56
|
+
return __assign({}, this._setters_);
|
57
|
+
},
|
58
|
+
enumerable: false,
|
59
|
+
configurable: true
|
60
|
+
});
|
61
|
+
Object.defineProperty(CleanStateBase.prototype, "initialState", {
|
62
|
+
get: function () {
|
63
|
+
return __assign({}, this._initialValues_);
|
64
|
+
},
|
65
|
+
enumerable: false,
|
66
|
+
configurable: true
|
67
|
+
});
|
68
|
+
CleanStateBase.update = function update() {
|
69
|
+
var _this = this;
|
70
|
+
if (!(this instanceof CleanState))
|
71
|
+
throw new Error('CleanState.update must be called with `this` value set to a CleanState instance. Did you forget to use `.call` or `.apply`? Example: CleanState.update.call(cleanState);');
|
72
|
+
/**
|
73
|
+
* Linters complain about the use of a React hook within a loop because:
|
74
|
+
* > By following this rule, you ensure that Hooks are called in the same order each time a component renders.
|
75
|
+
* > That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
|
76
|
+
* To resolve this, we're calling `useState` via an alias `retrieveState`.
|
77
|
+
* Bypassing this rule is safe here because `useCleanState` is a special case,
|
78
|
+
* and it guarantees that the same useState calls will be made on every render in the exact same order.
|
79
|
+
* Therefore, it is safe to silence the linters, and required for this implementation to work smoothly.
|
80
|
+
*/
|
81
|
+
var retrieveState = react_1.useState;
|
82
|
+
this.valueKeys.forEach(function (key) {
|
83
|
+
var _a;
|
84
|
+
_a = retrieveState(_this.initialState[key]), _this._values_[key] = _a[0], _this._setters_[key] = _a[1];
|
85
|
+
});
|
86
|
+
/* Object.entries<TUseStateArray<TState>>(stateAndSetters).forEach(([key, responseFromUseState]) => {
|
87
|
+
[this._values_[key], this._setters_[key]] = responseFromUseState;
|
88
|
+
}); */
|
56
89
|
// return this;
|
57
90
|
};
|
58
91
|
return CleanStateBase;
|
59
92
|
}());
|
60
93
|
;
|
61
|
-
var a;
|
62
94
|
var CleanState = CleanStateBase;
|
63
|
-
var na = new CleanState();
|
64
|
-
/**
|
65
|
-
* Linters complain about the use of a React hook within a loop because:
|
66
|
-
* > By following this rule, you ensure that Hooks are called in the same order each time a component renders.
|
67
|
-
* > That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
|
68
|
-
* To resolve this, we're calling `useState` via an alias `retrieveState`.
|
69
|
-
* Bypassing this rule is safe here because `useCleanState` is a special case,
|
70
|
-
* and it guarantees that the same useState calls will be made on every render in the exact same order.
|
71
|
-
* Therefore, it is safe to silence the linters, and required for this implementation to work smoothly.
|
72
|
-
*/
|
73
|
-
var retrieveState = react_1.useState;
|
74
95
|
var useCleanState = function (_initialState, props) {
|
75
|
-
var initialState = typeof _initialState === 'function' ? _initialState(props) : _initialState;
|
76
|
-
|
77
|
-
|
78
|
-
var stateKeys = Object.keys(initialState);
|
79
|
-
var initialCount = (0, react_1.useState)(stateKeys.length)[0];
|
80
|
-
if (stateKeys.length !== initialCount) {
|
81
|
-
throw new Error('The keys in your state object must be consistent throughout your components lifetime. Look up "rules of hooks" for more context.');
|
82
|
-
}
|
83
|
-
var stateAndSetters = {};
|
84
|
-
for (var _i = 0, stateKeys_1 = stateKeys; _i < stateKeys_1.length; _i++) {
|
85
|
-
var key = stateKeys_1[_i];
|
86
|
-
stateAndSetters[key] = retrieveState(initialState[key]);
|
87
|
-
}
|
88
|
-
CleanState.update.call(cleanState, stateAndSetters);
|
96
|
+
var initialState = typeof _initialState === 'function' ? (0, react_1.useMemo)(function () { return _initialState(props); }, []) : _initialState;
|
97
|
+
var cleanState = (0, react_1.useMemo)(function () { return new CleanState(initialState); }, []);
|
98
|
+
CleanState.update.call(cleanState);
|
89
99
|
return cleanState;
|
90
100
|
};
|
91
101
|
exports.useCleanState = useCleanState;
|
package/build/classy/class.d.ts
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
import type {
|
1
|
+
import type { ReactElement } from 'react';
|
2
2
|
import type { ComponentInstanceConstructor } from './instance';
|
3
3
|
import { ComponentInstance } from './instance';
|
4
4
|
type Obj = Record<string, any>;
|
5
5
|
type IComponentConstructor = ComponentInstanceConstructor<any, any, any> & typeof ClassComponent<any, any, any>;
|
6
6
|
export declare class ClassComponent<TState extends Obj, TProps extends Obj, THooks extends Obj> extends ComponentInstance<TState, TProps, THooks> {
|
7
|
-
Render:
|
8
|
-
|
9
|
-
* Use this to let React know whenever you would like all of your instance's state to be reset.
|
10
|
-
* When the value is changed, React will reset all state variables to their initial value the next time your component re-renders.
|
11
|
-
* @see https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes
|
12
|
-
*/
|
13
|
-
instanceId?: string;
|
14
|
-
static FC: <IComponentType extends IComponentConstructor>(this: IComponentType, _Component?: IComponentType) => (props: InstanceType<IComponentType>["props"]) => JSX.Element;
|
7
|
+
Render: () => ReactElement<any, any> | null;
|
8
|
+
static FC: <IComponentType extends IComponentConstructor>(this: IComponentType, _Component?: IComponentType) => (props: InstanceType<IComponentType>["props"]) => ReactElement<any, any> | null;
|
15
9
|
}
|
10
|
+
type AnyFunction = (...args: any) => any;
|
11
|
+
interface HookWrapperProps<THookFunction extends AnyFunction> {
|
12
|
+
hook: THookFunction;
|
13
|
+
argumentsList: Parameters<THookFunction>;
|
14
|
+
onUpdate: (output: ReturnType<THookFunction>) => void;
|
15
|
+
}
|
16
|
+
type ClassComponentHookWrapper = <Hook extends AnyFunction>(props: HookWrapperProps<Hook>) => null;
|
17
|
+
export declare const Use: ClassComponentHookWrapper;
|
16
18
|
export {};
|
package/build/classy/class.js
CHANGED
@@ -15,8 +15,7 @@ var __extends = (this && this.__extends) || (function () {
|
|
15
15
|
};
|
16
16
|
})();
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
18
|
-
exports.ClassComponent = void 0;
|
19
|
-
var jsx_runtime_1 = require("react/jsx-runtime");
|
18
|
+
exports.Use = exports.ClassComponent = void 0;
|
20
19
|
var react_1 = require("react");
|
21
20
|
var instance_1 = require("./instance");
|
22
21
|
/** Provide more useful stack traces for otherwise non-specific function names. */
|
@@ -43,16 +42,30 @@ var ClassComponent = /** @class */ (function (_super) {
|
|
43
42
|
if (!Component.getInitialState || !isClassComponentType)
|
44
43
|
throw new Error('Attempted to initialize ClassComponent with invalid Class type. Either pass a class that extends ClassComponent to FC (e.g `export FC(MyComponent);`), or ensure it is called as a method on a ClassComponent constructor type (e.g `export MyComponent.FC()`).');
|
45
44
|
var Wrapper = function (props) {
|
46
|
-
var
|
45
|
+
var Render = (0, instance_1.useInstance)(Component, props).Render;
|
47
46
|
// Add calling component name to Render function name in stack traces.
|
48
|
-
(0, react_1.useMemo)(function () { return setFunctionName(Render, "".concat(Component.name, "
|
49
|
-
|
47
|
+
(0, react_1.useMemo)(function () { return setFunctionName(Render, "".concat(Component.name, " > Render")); }, [Render]);
|
48
|
+
/**
|
49
|
+
* It may be impossible to set state within the body of Render,
|
50
|
+
* since technically, the Wrapper component owns the state and not the Render component.
|
51
|
+
* Consider using this as a function call instead of JSX to avoid that.
|
52
|
+
*/
|
53
|
+
// if (instance.renderAs === 'component') return <Render />;
|
54
|
+
return Render();
|
50
55
|
};
|
51
56
|
// Include calling component name in wrapper function name on stack traces.
|
52
|
-
|
53
|
-
setFunctionName(Wrapper, wrapperName);
|
57
|
+
setFunctionName(Wrapper, "".concat(Component.name, " > ").concat(Wrapper.name));
|
54
58
|
return Wrapper;
|
55
59
|
};
|
56
60
|
return ClassComponent;
|
57
61
|
}(instance_1.ComponentInstance));
|
58
62
|
exports.ClassComponent = ClassComponent;
|
63
|
+
var Use = function (_a) {
|
64
|
+
var useGenericHook = _a.hook, argumentsList = _a.argumentsList, onUpdate = _a.onUpdate;
|
65
|
+
var output = useGenericHook.apply(void 0, argumentsList);
|
66
|
+
(0, react_1.useEffect)(function () {
|
67
|
+
onUpdate(output);
|
68
|
+
}, [output]);
|
69
|
+
return null;
|
70
|
+
};
|
71
|
+
exports.Use = Use;
|
package/build/classy/instance.js
CHANGED
@@ -105,20 +105,17 @@ var useInstance = function (Component, props) {
|
|
105
105
|
var instance = (0, logic_1.useLogic)(Component, props);
|
106
106
|
// beforeMount, onMount, cleanUp.
|
107
107
|
(0, exports.useMountCallbacks)(instance);
|
108
|
+
// beforeRender.
|
108
109
|
(_a = instance.beforeRender) === null || _a === void 0 ? void 0 : _a.call(instance);
|
110
|
+
// onRender.
|
109
111
|
(0, react_1.useEffect)(function () {
|
110
112
|
var _a;
|
111
113
|
var cleanupAfterRerender = (_a = instance.onRender) === null || _a === void 0 ? void 0 : _a.call(instance);
|
112
114
|
return function () {
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
doCleanUp(cleanupAfterRerender);
|
118
|
-
}
|
119
|
-
else {
|
120
|
-
cleanupAfterRerender === null || cleanupAfterRerender === void 0 ? void 0 : cleanupAfterRerender.then(doCleanUp);
|
121
|
-
}
|
115
|
+
if (typeof cleanupAfterRerender === 'function')
|
116
|
+
cleanupAfterRerender();
|
117
|
+
else
|
118
|
+
cleanupAfterRerender === null || cleanupAfterRerender === void 0 ? void 0 : cleanupAfterRerender.then(function (cleanUp) { return cleanUp === null || cleanUp === void 0 ? void 0 : cleanUp(); });
|
122
119
|
};
|
123
120
|
});
|
124
121
|
return instance;
|
package/build/classy/logic.js
CHANGED
@@ -1,4 +1,15 @@
|
|
1
1
|
"use strict";
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
3
|
+
__assign = Object.assign || function(t) {
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
5
|
+
s = arguments[i];
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
7
|
+
t[p] = s[p];
|
8
|
+
}
|
9
|
+
return t;
|
10
|
+
};
|
11
|
+
return __assign.apply(this, arguments);
|
12
|
+
};
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
14
|
exports.useLogic = exports.ComponentLogic = void 0;
|
4
15
|
var react_1 = require("react");
|
@@ -25,6 +36,7 @@ var useLogic = function (Methods, props) {
|
|
25
36
|
methods.state = state;
|
26
37
|
methods.props = props;
|
27
38
|
methods.hooks = ((_a = methods.useHooks) === null || _a === void 0 ? void 0 : _a.call(methods)) || {};
|
28
|
-
|
39
|
+
// Return a gate object to "passthrough" all methods but filter out properties that should be private.
|
40
|
+
return __assign(__assign({}, methods), { useHooks: undefined });
|
29
41
|
};
|
30
42
|
exports.useLogic = useLogic;
|
package/build/index.js
CHANGED
@@ -17,13 +17,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./classy"), exports);
|
18
18
|
// PS: Document component inheritance pattern with lifecycle callback arrays and namespaces.
|
19
19
|
// Due to react's remounting behaviour, components must externally track when some logic has run, if it really really must only ever run once per mounted instance. Tricky to get right for components that may have multiple instance rendered simultaneously at different parts of a page.
|
20
|
-
//
|
21
|
-
//
|
22
|
-
// But setState might ignore calls with the same object ref as the existing state value, so perhaps create a new cleanstate
|
23
|
-
// instance instead, spreading existing values with the changed values, and call setState with that.
|
24
|
-
// It could be more performant as it would remove the need for looping over Object.keys in useCleanState.
|
25
|
-
// Investigate this for a potential minor version update.
|
26
|
-
// useCleanState => useState
|
20
|
+
// useCleanState => useState, separate call for each key
|
21
|
+
// useMergedState => useState, same call for all keys
|
27
22
|
// useMethods => useCallback
|
28
23
|
// useLogic => useCallback + all other hook calls.
|
29
24
|
// useInstance => useLogic + lifecycle methods.
|
25
|
+
/*
|
26
|
+
- Write usage doc
|
27
|
+
- Push to git and publish to site
|
28
|
+
- Follow-up personal post on class component inheritance can be published on GH Pages from profile repo.
|
29
|
+
- Publish to NPM
|
30
|
+
- Finalize package and repo names, then post to Twitter.
|
31
|
+
*/
|
32
|
+
/*
|
33
|
+
withFetchApi(baseUrl); To mimic axios.get and axios.post type calls.
|
34
|
+
@cleanweb/mem-store - Release global-store package here.
|
35
|
+
Use mem-store to cache requests in createApi(); Use md5 hashed url as key.
|
36
|
+
@todo Add simple persistence layer with indexed db.
|
37
|
+
@cleanweb/subscribable - To publish changes in the data to subscribers.
|
38
|
+
@cleanweb/reactive-data - To combine all 4.
|
39
|
+
*/
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@cleanweb/react",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.8",
|
4
4
|
"description": "A suite of helpers for writing cleaner React function components.",
|
5
5
|
"engines": {
|
6
6
|
"node": ">=18"
|
@@ -20,6 +20,7 @@
|
|
20
20
|
"./base": "./build/base/index.js",
|
21
21
|
"./classy": "./build/classy/index.js",
|
22
22
|
"./state": "./build/base/state.js",
|
23
|
+
"./state/merged": "./build/base/merged-state.js",
|
23
24
|
"./methods": "./build/base/methods.js",
|
24
25
|
"./logic": "./build/classy/logic.js",
|
25
26
|
"./instance": "./build/classy/instance.js",
|