@cleanweb/react 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }