@cleanweb/react 1.0.7 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
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
- ## useCleanState
5
- Example:
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
- const state1 = useCleanState(initialStateObject); // { myValue: 'initial-value' }
8
- const state2 = useCleanState(initialStateGetter); // () => ({ myValue: 'initial-value' })
9
-
10
- return (
11
- {/* or state2.put.clicked(true) or state2.putMany({ clicked: true }) */}
12
- <button onClick={() => state2.clicked = true }>
13
- {state1.label}
14
- </button>
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
- ## useMethods
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
- ## useLogic
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
- ## useInstance
25
- To be finished
26
- Define all of your component's logic and lifecycle effects in a seperate class. Allow your function component to remain neat as just a JSX template. Access the instance of your class within the template with the useInstance hook.
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
- ## ClassComponent
29
- To be finished.
30
- Wrap your function component in a class, allowing you to organize logic into discreet methods and work with a persistent instance throughout the component's lifecycle that's much easier to reason about. At it's core, your component remains a function component and maintains all features of function components.
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,15 +1,4 @@
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
- };
13
2
  Object.defineProperty(exports, "__esModule", { value: true });
14
3
  exports.useMethods = exports.ComponentMethods = void 0;
15
4
  var react_1 = require("react");
@@ -27,7 +16,6 @@ var useMethods = function (Methods, state, props) {
27
16
  }, []);
28
17
  methods.state = state;
29
18
  methods.props = props;
30
- // Return a gate object to "passthrough" all methods but filter out properties that should be private.
31
- return __assign(__assign({}, methods), { props: undefined, state: undefined });
19
+ return methods;
32
20
  };
33
21
  exports.useMethods = useMethods;
@@ -1,16 +1,18 @@
1
- import type { FunctionComponent } from 'react';
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: FunctionComponent<TProps>;
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 {};
@@ -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 _a = (0, instance_1.useInstance)(Component, props), Render = _a.Render, instanceId = _a.instanceId;
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, ".Render")); }, []);
49
- return (0, jsx_runtime_1.jsx)(Render, {}, instanceId);
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
- var wrapperName = "ClassComponent".concat(Wrapper.name, " > ").concat(Component.name);
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;
@@ -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
- var doCleanUp = function (runRenderCleanup) {
114
- runRenderCleanup === null || runRenderCleanup === void 0 ? void 0 : runRenderCleanup();
115
- };
116
- if (typeof cleanupAfterRerender === 'function') {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleanweb/react",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "A suite of helpers for writing cleaner React function components.",
5
5
  "engines": {
6
6
  "node": ">=18"