@cleanweb/react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ import type { ComponentInstanceConstructor } from '@/instance';
2
+ import { FunctionComponent } from "react";
3
+ import { ComponentInstance } from '@/instance';
4
+ type Obj = Record<string, any>;
5
+ type IComponentConstructor = ComponentInstanceConstructor<any, any, any> & typeof ClassComponent<any, any, any>;
6
+ export declare class ClassComponent<TState extends Obj, TProps extends Obj, THooks extends Obj> extends ComponentInstance<TState, TProps, THooks> {
7
+ Render: FunctionComponent<TProps>;
8
+ static FC: <IComponentType extends IComponentConstructor>(this: IComponentType, _Component?: IComponentType) => (props: InstanceType<IComponentType>["props"]) => import("react").JSX.Element;
9
+ }
10
+ export {};
@@ -0,0 +1,54 @@
1
+ var __extends = (this && this.__extends) || (function () {
2
+ var extendStatics = function (d, b) {
3
+ extendStatics = Object.setPrototypeOf ||
4
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
6
+ return extendStatics(d, b);
7
+ };
8
+ return function (d, b) {
9
+ if (typeof b !== "function" && b !== null)
10
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
11
+ extendStatics(d, b);
12
+ function __() { this.constructor = d; }
13
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
14
+ };
15
+ })();
16
+ import { useMemo } from "react";
17
+ import { ComponentInstance, useInstance } from '@/instance';
18
+ /** Provide more useful stack traces for otherwise non-specific function names. */
19
+ var setFunctionName = function (func, newName) {
20
+ try {
21
+ // Must use try block, as `name` is not configurable on older browsers, and may yield a TypeError.
22
+ Object.defineProperty(func, 'name', {
23
+ writable: true,
24
+ value: newName,
25
+ });
26
+ }
27
+ catch (error) {
28
+ console.warn(error);
29
+ }
30
+ };
31
+ var ClassComponent = /** @class */ (function (_super) {
32
+ __extends(ClassComponent, _super);
33
+ function ClassComponent() {
34
+ return _super !== null && _super.apply(this, arguments) || this;
35
+ }
36
+ ClassComponent.FC = function FC(_Component) {
37
+ var Component = _Component || this;
38
+ var isClassComponentType = Component.prototype instanceof ClassComponent;
39
+ if (!Component.getInitialState || !isClassComponentType)
40
+ 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()`).');
41
+ var Wrapper = function (props) {
42
+ var Render = useInstance(Component, props).Render;
43
+ // Add calling component name to Render function name in stack traces.
44
+ useMemo(function () { return setFunctionName(Render, "".concat(Component.name, ".Render")); }, []);
45
+ return <Render />;
46
+ };
47
+ // Include calling component name in wrapper function name on stack traces.
48
+ var wrapperName = "ClassComponent".concat(Wrapper.name, " > ").concat(Component.name);
49
+ setFunctionName(Wrapper, wrapperName);
50
+ return Wrapper;
51
+ };
52
+ return ClassComponent;
53
+ }(ComponentInstance));
54
+ export { ClassComponent };
@@ -0,0 +1,52 @@
1
+
2
+ type Optional<
3
+ BaseType,
4
+ AllowNull extends boolean = true
5
+ > = (
6
+ AllowNull extends true
7
+ ? BaseType | undefined | null
8
+ : BaseType | undefined
9
+ )
10
+
11
+ type Constructor<
12
+ TInstance extends any = any,
13
+ TParams extends any[] = never[]
14
+ > = new (...args: TParams) => TInstance
15
+
16
+
17
+ /**
18
+ * @example
19
+ * ```js
20
+ * const getNumber: AsyncFunction<number> = async () => {
21
+ * await oneTickDelay();
22
+ * return 5;
23
+ * }
24
+ * ```
25
+ */
26
+ declare type AsyncFunction<
27
+ TReturnValue extends any = void,
28
+ Params extends any[] = never[]
29
+ > = (...params: Params) => Promise<TReturnValue>
30
+
31
+ /**
32
+ * A function that takes no arguments and returns nothing.
33
+ * Pass a type argument to set whether `async` and/or `sync` functions are allowed.
34
+ */
35
+ declare interface IVoidFunction<AsyncType extends 'async' | 'sync' | 'both' = 'both'> {
36
+ (): AsyncType extends 'async' ? Promise<void>
37
+ : AsyncType extends 'sync' ? void
38
+ : Promise<void> | void
39
+ }
40
+
41
+ declare interface Window {
42
+ }
43
+
44
+ declare namespace JSX {
45
+ interface IntrinsicElements {
46
+ }
47
+ }
48
+
49
+ declare namespace NodeJS {
50
+ interface ProcessEnv {
51
+ }
52
+ }
@@ -0,0 +1,18 @@
1
+ import { ComponentLogic, ComponentLogicConstructor } from "./logic";
2
+ type Obj = Record<string, any>;
3
+ type AsyncAllowedEffectCallback = () => IVoidFunction | Promise<IVoidFunction>;
4
+ export declare const noOp: () => void;
5
+ export declare class ComponentInstance<TState extends Obj = {}, TProps extends Obj = {}, THooks extends Obj = {}> extends ComponentLogic<TState, TProps, THooks> {
6
+ beforeMount: IVoidFunction;
7
+ onMount: AsyncAllowedEffectCallback;
8
+ beforeRender: IVoidFunction;
9
+ onRender: AsyncAllowedEffectCallback;
10
+ cleanUp: IVoidFunction;
11
+ }
12
+ type ComponentClassBaseType<TState extends Obj = {}, TProps extends Obj = {}, THooks extends Obj = {}> = ComponentLogicConstructor<TState, TProps, THooks> & Constructor<ComponentInstance<TState, TProps, THooks>>;
13
+ export interface ComponentInstanceConstructor<TState extends Obj = {}, TProps extends Obj = {}, THooks extends Obj = {}> extends ComponentClassBaseType<TState, TProps, THooks> {
14
+ }
15
+ type UseInstance = <TClass extends ComponentInstanceConstructor>(Class: TClass, props: InstanceType<TClass>['props']) => InstanceType<TClass>;
16
+ export declare const useMountCallbacks: <TInstance extends ComponentInstance<any, any, any>>(instance: TInstance) => void;
17
+ export declare const useInstance: UseInstance;
18
+ export {};
@@ -0,0 +1,81 @@
1
+ var __extends = (this && this.__extends) || (function () {
2
+ var extendStatics = function (d, b) {
3
+ extendStatics = Object.setPrototypeOf ||
4
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
6
+ return extendStatics(d, b);
7
+ };
8
+ return function (d, b) {
9
+ if (typeof b !== "function" && b !== null)
10
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
11
+ extendStatics(d, b);
12
+ function __() { this.constructor = d; }
13
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
14
+ };
15
+ })();
16
+ import { useEffect } from "react";
17
+ import { useMountState } from "./state";
18
+ import { ComponentLogic, useLogic } from "./logic";
19
+ export var noOp = function () { };
20
+ var ComponentInstance = /** @class */ (function (_super) {
21
+ __extends(ComponentInstance, _super);
22
+ function ComponentInstance() {
23
+ var _this = _super !== null && _super.apply(this, arguments) || this;
24
+ _this.beforeMount = function () { };
25
+ _this.onMount = function () { return noOp; };
26
+ _this.beforeRender = function () { };
27
+ _this.onRender = function () { return noOp; };
28
+ _this.cleanUp = function () { };
29
+ return _this;
30
+ }
31
+ return ComponentInstance;
32
+ }(ComponentLogic));
33
+ export { ComponentInstance };
34
+ ;
35
+ export var useMountCallbacks = function (instance) {
36
+ var _a;
37
+ var mounted = useMountState();
38
+ if (!mounted)
39
+ (_a = instance.beforeMount) === null || _a === void 0 ? void 0 : _a.call(instance);
40
+ useEffect(function () {
41
+ var _a;
42
+ var mountHandlerCleanUp = (_a = instance.onMount) === null || _a === void 0 ? void 0 : _a.call(instance);
43
+ return function () {
44
+ var doCleanUp = function (runMountCleaners) {
45
+ var _a;
46
+ runMountCleaners === null || runMountCleaners === void 0 ? void 0 : runMountCleaners();
47
+ (_a = instance.cleanUp) === null || _a === void 0 ? void 0 : _a.call(instance);
48
+ };
49
+ if (typeof mountHandlerCleanUp === 'function') {
50
+ doCleanUp(mountHandlerCleanUp);
51
+ }
52
+ else {
53
+ mountHandlerCleanUp === null || mountHandlerCleanUp === void 0 ? void 0 : mountHandlerCleanUp.then(doCleanUp);
54
+ }
55
+ };
56
+ }, []);
57
+ };
58
+ export var useInstance = function (Component, props) {
59
+ var _a;
60
+ // useCustomHooks.
61
+ var instance = useLogic(Component, props);
62
+ // beforeMount, onMount, cleanUp.
63
+ useMountCallbacks(instance);
64
+ (_a = instance.beforeRender) === null || _a === void 0 ? void 0 : _a.call(instance);
65
+ useEffect(function () {
66
+ var _a;
67
+ var cleanupAfterRerender = (_a = instance.onRender) === null || _a === void 0 ? void 0 : _a.call(instance);
68
+ return function () {
69
+ var doCleanUp = function (runRenderCleanup) {
70
+ runRenderCleanup === null || runRenderCleanup === void 0 ? void 0 : runRenderCleanup();
71
+ };
72
+ if (typeof cleanupAfterRerender === 'function') {
73
+ doCleanUp(cleanupAfterRerender);
74
+ }
75
+ else {
76
+ cleanupAfterRerender === null || cleanupAfterRerender === void 0 ? void 0 : cleanupAfterRerender.then(doCleanUp);
77
+ }
78
+ };
79
+ });
80
+ return instance;
81
+ };
@@ -0,0 +1,13 @@
1
+ import type { CleanState } from "./state";
2
+ export declare class ComponentLogic<TState extends object, TProps extends object, THooks extends object> {
3
+ state: CleanState<TState>;
4
+ props: TProps;
5
+ hooks: THooks;
6
+ useCustomHooks?: () => THooks;
7
+ }
8
+ export interface ComponentLogicConstructor<TState extends object, TProps extends object, THooks extends object> extends Constructor<ComponentLogic<TState, TProps, THooks>> {
9
+ getInitialState: (props?: TProps) => TState;
10
+ }
11
+ type UseLogic = <TState extends object, LogicClass extends ComponentLogicConstructor<TState, any, any>>(Methods: LogicClass, props: InstanceType<LogicClass>['props']) => InstanceType<LogicClass>;
12
+ export declare const useLogic: UseLogic;
13
+ export {};
package/build/logic.js ADDED
@@ -0,0 +1,26 @@
1
+ import { useMemo } from "react";
2
+ import { useCleanState } from "./state";
3
+ var ComponentLogic = /** @class */ (function () {
4
+ function ComponentLogic() {
5
+ }
6
+ return ComponentLogic;
7
+ }());
8
+ export { ComponentLogic };
9
+ ;
10
+ export var useLogic = function (Methods, props) {
11
+ var _a;
12
+ var state = useCleanState(Methods.getInitialState, props);
13
+ // There's apparently a bug? with Typescript that pegs the return type of "new Methods()" to "ComponentLogic<{}, {}, {}>",
14
+ // completely ignoring the type specified for Methods in the function's type definition.
15
+ // `new Methods()` should return whatever the InstanceType of TClass is, as that is the type explicitly specified for Methods.
16
+ // Ignoring the specified type to gin up something else and then complain about it is quite weird.
17
+ // Regardless, even when `extends ComponentLogicConstructor<TState, TProps, THooks>` is specified using generics instead of a set type,
18
+ // the issue persists. Which is absurd since this should ensure that InstanceType<Class> should exactly match ComponentLogic<TState, TProps, THooks>
19
+ var methods = useMemo(function () {
20
+ return new Methods();
21
+ }, []);
22
+ methods.state = state;
23
+ methods.props = props;
24
+ methods.hooks = ((_a = methods.useCustomHooks) === null || _a === void 0 ? void 0 : _a.call(methods)) || {};
25
+ return methods;
26
+ };
@@ -0,0 +1,9 @@
1
+ import type { CleanState } from "./state";
2
+ export declare class ComponentMethods<TState extends object, TProps extends object> {
3
+ state: CleanState<TState>;
4
+ props: TProps;
5
+ }
6
+ type ComponentMethodsConstructor = typeof ComponentMethods<any, any>;
7
+ type UseMethods = <MethodsClass extends ComponentMethodsConstructor>(Methods: MethodsClass, state: InstanceType<MethodsClass>['state'], props: InstanceType<MethodsClass>['props']) => InstanceType<MethodsClass>;
8
+ export declare const useMethods: UseMethods;
9
+ export {};
@@ -0,0 +1,29 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { useMemo } from "react";
13
+ var ComponentMethods = /** @class */ (function () {
14
+ function ComponentMethods() {
15
+ }
16
+ return ComponentMethods;
17
+ }());
18
+ export { ComponentMethods };
19
+ ;
20
+ export var useMethods = function (Methods, state, props) {
21
+ var methods = useMemo(function () {
22
+ // See useLogic implementation for a discussion of this type assertion.
23
+ return new Methods();
24
+ }, []);
25
+ methods.state = state;
26
+ methods.props = props;
27
+ // Return a gate object to "passthrough" all methods but filter out properties that should be private.
28
+ return __assign(__assign({}, methods), { props: undefined, state: undefined });
29
+ };
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cleanweb/react",
3
+ "version": "1.0.0",
4
+ "description": "A suite of helpers for writing cleaner React function components.",
5
+ "engines": {
6
+ "node": ">=18"
7
+ },
8
+ "files": [
9
+ "build"
10
+ ],
11
+ "scripts": {
12
+ "build": "rimraf ./build && tsc && copyfiles README.md package.json .npmrc globals.d.ts tsconfig.json build",
13
+ "prepublishOnly": "npm run build",
14
+ "postpublish": "cd ./mirror-pkg && npm publish && cd ..",
15
+ "test": "echo \"No tests ATM\""
16
+ },
17
+ "keywords": [
18
+ "react",
19
+ "clean-react",
20
+ "clean react",
21
+ "function components",
22
+ "hooks",
23
+ "react hooks",
24
+ "state",
25
+ "clean state",
26
+ "group state"
27
+ ],
28
+ "author": {
29
+ "name": "Feranmi Akinlade",
30
+ "url": "https://feranmi.dev"
31
+ },
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@types/node": "20.14.10",
35
+ "@types/react": "^16",
36
+ "copyfiles": "^2.4.1",
37
+ "rimraf": "^6.0.1",
38
+ "typescript": "^5.6.2"
39
+ },
40
+ "peerDependencies": {
41
+ "react": ">=16"
42
+ }
43
+ }
@@ -0,0 +1,26 @@
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
+ declare class _CleanState_<TState extends object> {
9
+ private _values_;
10
+ private _setters_;
11
+ put: { [Key in keyof TState]: (value: TState[Key]) => void; };
12
+ constructor(stateAndSetters: TUseStateResponses<TState>);
13
+ putMany: (newValues: Partial<TState>) => void;
14
+ }
15
+ type TCleanStateInstance<TState extends object> = TState & _CleanState_<TState>;
16
+ declare const _CleanState: new <TState extends object>(...args: ConstructorParameters<typeof _CleanState_>) => TCleanStateInstance<TState>;
17
+ export type CleanState<TState extends object> = InstanceType<typeof _CleanState<TState>>;
18
+ type Func = (...params: any[]) => any;
19
+ type UseCleanState = <TStateObjOrFactory extends ((props?: TProps) => object) | object, TProps extends object = object>(_initialState: TStateObjOrFactory, props?: TStateObjOrFactory extends Func ? TProps : undefined) => CleanState<TStateObjOrFactory extends Func ? ReturnType<TStateObjOrFactory> : TStateObjOrFactory>;
20
+ export declare const useCleanState: UseCleanState;
21
+ /**
22
+ * Returns a value that is false before the component has been mounted,
23
+ * then true during all subsequent rerenders.
24
+ */
25
+ export declare const useMountState: () => boolean;
26
+ export {};
package/build/state.js ADDED
@@ -0,0 +1,81 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ var _CleanState_ = /** @class */ (function () {
3
+ function _CleanState_(stateAndSetters) {
4
+ var _this = this;
5
+ this._values_ = {};
6
+ this._setters_ = {};
7
+ this.put = this._setters_;
8
+ this.putMany = function (newValues) {
9
+ Object.entries(newValues).forEach(function (_a) {
10
+ var key = _a[0], value = _a[1];
11
+ _this.put[key](value);
12
+ });
13
+ };
14
+ /** Must be extracted before the loop begins to avoid including keys from the consumers state object. */
15
+ var reservedKeys = Object.keys(this);
16
+ Object.entries(stateAndSetters).forEach(function (_a) {
17
+ var key = _a[0], responseFromUseState = _a[1];
18
+ if (reservedKeys.includes(key))
19
+ throw new Error("The name \"".concat(key, "\" is reserved by CleanState and cannot be used to index state variables. Please use a different key."));
20
+ _this._values_[key] = responseFromUseState[0], _this._setters_[key] = responseFromUseState[1];
21
+ _this.put[key] = _this._setters_[key];
22
+ var self = _this;
23
+ Object.defineProperty(_this, key, {
24
+ get: function () {
25
+ return self._values_[key];
26
+ },
27
+ set: function (value) {
28
+ self._setters_[key](value);
29
+ },
30
+ enumerable: true,
31
+ });
32
+ });
33
+ }
34
+ return _CleanState_;
35
+ }());
36
+ ;
37
+ var _CleanState = _CleanState_;
38
+ /**
39
+ * Linters complain about the use of a React hook within a loop because:
40
+ * > By following this rule, you ensure that Hooks are called in the same order each time a component renders.
41
+ * > That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
42
+ * To resolve this, we're calling `useState` via an alias `retrieveState`.
43
+ * Bypassing this rule is safe here because `useCleanState` is a special case,
44
+ * and it guarantees that the same useState calls will be made on every render in the exact same order.
45
+ * Therefore, it is safe to silence the linters, and required for this implementation to work smoothly.
46
+ */
47
+ var retrieveState = useState;
48
+ export var useCleanState = function (_initialState, props) {
49
+ var initialState = typeof _initialState === 'function' ? _initialState(props) : _initialState;
50
+ var stateKeys = Object.keys(initialState);
51
+ var initialCount = useState(stateKeys.length)[0];
52
+ if (stateKeys.length !== initialCount) {
53
+ throw new Error('The keys in your state object must be consistent throughout your components lifetime. Look up "rules of hooks" for more context.');
54
+ }
55
+ var stateAndSetters = {};
56
+ for (var _i = 0, stateKeys_1 = stateKeys; _i < stateKeys_1.length; _i++) {
57
+ var key = stateKeys_1[_i];
58
+ stateAndSetters[key] = retrieveState(initialState[key]);
59
+ }
60
+ // @todo Refactor to return consistent state instance each render.
61
+ // Though useState gives us persistent references for values and setters,
62
+ // so keeping the CleanState wrapper persistent may be unnecessary.
63
+ return new _CleanState(stateAndSetters);
64
+ };
65
+ /**
66
+ * Returns a value that is false before the component has been mounted,
67
+ * then true during all subsequent rerenders.
68
+ */
69
+ export var useMountState = function () {
70
+ /**
71
+ * This must not be a stateful value. It should not be the cause of a rerender.
72
+ * It merely provides information about the render count,
73
+ * without influencing that count itself.
74
+ * So `mounted` should never be set with `useState`.
75
+ */
76
+ var mounted = useRef(false);
77
+ useEffect(function () {
78
+ mounted.current = true;
79
+ }, []);
80
+ return mounted.current;
81
+ };
@@ -0,0 +1,43 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "rootDir": ".",
5
+ "outDir": "build",
6
+ "paths": {
7
+ "@/*": [
8
+ "./*"
9
+ ]
10
+ },
11
+ "target": "es5",
12
+ "lib": [
13
+ "dom",
14
+ "dom.iterable",
15
+ "esnext"
16
+ ],
17
+ "allowJs": true,
18
+ "checkJs": true,
19
+ "declaration": true,
20
+ "noEmit": false,
21
+ "skipLibCheck": true,
22
+ "strict": false,
23
+ "forceConsistentCasingInFileNames": true,
24
+ "incremental": false,
25
+ "esModuleInterop": true,
26
+ "module": "esnext",
27
+ "moduleResolution": "node",
28
+ "resolveJsonModule": true,
29
+ "isolatedModules": true,
30
+ "jsx": "preserve",
31
+ "strictNullChecks": true
32
+ },
33
+ "include": [
34
+ "next-env.d.ts",
35
+ "**/*.ts",
36
+ "**/*.tsx"
37
+ ],
38
+ "exclude": [
39
+ "node_modules/**/**.*",
40
+ "build/**/**.*",
41
+ "mirror-pkg/**/**.*"
42
+ ]
43
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cleanweb/react",
3
+ "version": "1.0.0",
4
+ "description": "A suite of helpers for writing cleaner React function components.",
5
+ "engines": {
6
+ "node": ">=18"
7
+ },
8
+ "files": [
9
+ "build"
10
+ ],
11
+ "scripts": {
12
+ "build": "rimraf ./build && tsc && copyfiles README.md package.json .npmrc globals.d.ts tsconfig.json build",
13
+ "prepublishOnly": "npm run build",
14
+ "postpublish": "cd ./mirror-pkg && npm publish && cd ..",
15
+ "test": "echo \"No tests ATM\""
16
+ },
17
+ "keywords": [
18
+ "react",
19
+ "clean-react",
20
+ "clean react",
21
+ "function components",
22
+ "hooks",
23
+ "react hooks",
24
+ "state",
25
+ "clean state",
26
+ "group state"
27
+ ],
28
+ "author": {
29
+ "name": "Feranmi Akinlade",
30
+ "url": "https://feranmi.dev"
31
+ },
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@types/node": "20.14.10",
35
+ "@types/react": "^16",
36
+ "copyfiles": "^2.4.1",
37
+ "rimraf": "^6.0.1",
38
+ "typescript": "^5.6.2"
39
+ },
40
+ "peerDependencies": {
41
+ "react": ">=16"
42
+ }
43
+ }