Skip to content

Commit

Permalink
Improve types of hook source (#4229)
Browse files Browse the repository at this point in the history
* Improve hook src typings

* Use `unknown` instead of `any` when we explicitly don't know the type

Also improve typings of options used in hooks

* Add return types to functions
  • Loading branch information
andrewiggins authored Dec 14, 2023
1 parent aa53ecb commit 4f2a04f
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 63 deletions.
79 changes: 59 additions & 20 deletions hooks/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { options } from 'preact';
import { options as _options } from 'preact';

/** @type {number} */
let currentIndex;
Expand All @@ -17,6 +17,9 @@ let afterPaintEffects = [];

let EMPTY = [];

// Cast to use internal Options type
const options = /** @type {import('./internal').Options} */ (_options);

let oldBeforeDiff = options._diff;
let oldBeforeRender = options._render;
let oldAfterDiff = options.diffed;
Expand All @@ -26,11 +29,13 @@ let oldBeforeUnmount = options.unmount;
const RAF_TIMEOUT = 100;
let prevRaf;

/** @type {(vnode: import('./internal').VNode) => void} */
options._diff = vnode => {
currentComponent = null;
if (oldBeforeDiff) oldBeforeDiff(vnode);
};

/** @type {(vnode: import('./internal').VNode) => void} */
options._render = vnode => {
if (oldBeforeRender) oldBeforeRender(vnode);

Expand Down Expand Up @@ -59,6 +64,7 @@ options._render = vnode => {
previousComponent = currentComponent;
};

/** @type {(vnode: import('./internal').VNode) => void} */
options.diffed = vnode => {
if (oldAfterDiff) oldAfterDiff(vnode);

Expand All @@ -79,6 +85,8 @@ options.diffed = vnode => {
previousComponent = currentComponent = null;
};

// TODO: Improve typing of commitQueue parameter
/** @type {(vnode: import('./internal').VNode, commitQueue: any) => void} */
options._commit = (vnode, commitQueue) => {
commitQueue.some(component => {
try {
Expand All @@ -98,6 +106,7 @@ options._commit = (vnode, commitQueue) => {
if (oldCommit) oldCommit(vnode, commitQueue);
};

/** @type {(vnode: import('./internal').VNode) => void} */
options.unmount = vnode => {
if (oldBeforeUnmount) oldBeforeUnmount(vnode);

Expand Down Expand Up @@ -143,22 +152,27 @@ function getHookState(index, type) {
if (index >= hooks._list.length) {
hooks._list.push({ _pendingValue: EMPTY });
}

return hooks._list[index];
}

/**
* @param {import('./index').StateUpdater<any>} [initialState]
* @template {unknown} S
* @param {import('./index').StateUpdater<S>} [initialState]
* @returns {[S, (state: S) => void]}
*/
export function useState(initialState) {
currentHook = 1;
return useReducer(invokeOrReturn, initialState);
}

/**
* @param {import('./index').Reducer<any, any>} reducer
* @param {import('./index').StateUpdater<any>} initialState
* @template {unknown} S
* @template {unknown} A
* @param {import('./index').Reducer<S, A>} reducer
* @param {import('./index').StateUpdater<S>} initialState
* @param {(initialState: any) => void} [init]
* @returns {[ any, (state: any) => void ]}
* @returns {[ S, (state: S) => void ]}
*/
export function useReducer(reducer, initialState, init) {
/** @type {import('./internal').ReducerHookState} */
Expand Down Expand Up @@ -218,9 +232,11 @@ export function useReducer(reducer, initialState, init) {
function updateHookState(p, s, c) {
if (!hookState._component.__hooks) return true;

const stateHooks = hookState._component.__hooks._list.filter(
x => x._component
);
/** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
const isStateHook = x => !!x._component;
const stateHooks =
hookState._component.__hooks._list.filter(isStateHook);

const allHooksEmpty = stateHooks.every(x => !x._nextValue);
// When we have no updated hooks in the component we invoke the previous SCU or
// traverse the VDOM tree further.
Expand Down Expand Up @@ -257,7 +273,8 @@ export function useReducer(reducer, initialState, init) {

/**
* @param {import('./internal').Effect} callback
* @param {any[]} args
* @param {unknown[]} args
* @returns {void}
*/
export function useEffect(callback, args) {
/** @type {import('./internal').EffectHookState} */
Expand All @@ -272,7 +289,8 @@ export function useEffect(callback, args) {

/**
* @param {import('./internal').Effect} callback
* @param {any[]} args
* @param {unknown[]} args
* @returns {void}
*/
export function useLayoutEffect(callback, args) {
/** @type {import('./internal').EffectHookState} */
Expand All @@ -285,6 +303,7 @@ export function useLayoutEffect(callback, args) {
}
}

/** @type {(initialValue: unknown) => unknown} */
export function useRef(initialValue) {
currentHook = 5;
return useMemo(() => ({ current: initialValue }), []);
Expand All @@ -293,7 +312,8 @@ export function useRef(initialValue) {
/**
* @param {object} ref
* @param {() => object} createHandle
* @param {any[]} args
* @param {unknown[]} args
* @returns {void}
*/
export function useImperativeHandle(ref, createHandle, args) {
currentHook = 6;
Expand All @@ -312,11 +332,13 @@ export function useImperativeHandle(ref, createHandle, args) {
}

/**
* @param {() => any} factory
* @param {any[]} args
* @template {unknown} T
* @param {() => T} factory
* @param {unknown[]} args
* @returns {T}
*/
export function useMemo(factory, args) {
/** @type {import('./internal').MemoHookState} */
/** @type {import('./internal').MemoHookState<T>} */
const state = getHookState(currentIndex++, 7);
if (argsChanged(state._args, args)) {
state._pendingValue = factory();
Expand All @@ -330,7 +352,8 @@ export function useMemo(factory, args) {

/**
* @param {() => void} callback
* @param {any[]} args
* @param {unknown[]} args
* @returns {() => void}
*/
export function useCallback(callback, args) {
currentHook = 8;
Expand Down Expand Up @@ -366,12 +389,15 @@ export function useContext(context) {
*/
export function useDebugValue(value, formatter) {
if (options.useDebugValue) {
options.useDebugValue(formatter ? formatter(value) : value);
options.useDebugValue(
formatter ? formatter(value) : /** @type {any}*/ (value)
);
}
}

/**
* @param {(error: any, errorInfo: import('preact').ErrorInfo) => void} cb
* @param {(error: unknown, errorInfo: import('preact').ErrorInfo) => void} cb
* @returns {[unknown, () => void]}
*/
export function useErrorBoundary(cb) {
/** @type {import('./internal').ErrorBoundaryHookState} */
Expand All @@ -392,7 +418,9 @@ export function useErrorBoundary(cb) {
];
}

/** @type {() => string} */
export function useId() {
/** @type {import('./internal').IdHookState} */
const state = getHookState(currentIndex++, 11);
if (!state._value) {
// Grab either the root node or the nearest async boundary node.
Expand All @@ -408,6 +436,7 @@ export function useId() {

return state._value;
}

/**
* After paint effects consumer.
*/
Expand Down Expand Up @@ -458,6 +487,7 @@ function afterNextFrame(callback) {
/**
* Schedule afterPaintEffects flush after the browser paints
* @param {number} newQueueLength
* @returns {void}
*/
function afterPaint(newQueueLength) {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
Expand All @@ -467,7 +497,8 @@ function afterPaint(newQueueLength) {
}

/**
* @param {import('./internal').EffectHookState} hook
* @param {import('./internal').HookState} hook
* @returns {void}
*/
function invokeCleanup(hook) {
// A hook cleanup can introduce a call to render which creates a new root, this will call options.vnode
Expand All @@ -485,6 +516,7 @@ function invokeCleanup(hook) {
/**
* Invoke a Hook's effect
* @param {import('./internal').EffectHookState} hook
* @returns {void}
*/
function invokeEffect(hook) {
// A hook call can introduce a call to render which creates a new root, this will call options.vnode
Expand All @@ -495,8 +527,9 @@ function invokeEffect(hook) {
}

/**
* @param {any[]} oldArgs
* @param {any[]} newArgs
* @param {unknown[]} oldArgs
* @param {unknown[]} newArgs
* @returns {boolean}
*/
function argsChanged(oldArgs, newArgs) {
return (
Expand All @@ -506,6 +539,12 @@ function argsChanged(oldArgs, newArgs) {
);
}

/**
* @template Arg
* @param {Arg} arg
* @param {(arg: Arg) => any} f
* @returns {any}
*/
function invokeOrReturn(arg, f) {
return typeof f == 'function' ? f(arg) : f;
}
94 changes: 52 additions & 42 deletions hooks/src/internal.d.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
import {
Component as PreactComponent,
PreactContext,
ErrorInfo,
VNode as PreactVNode
} from '../../src/internal';
import { Reducer } from '.';
import { Reducer, StateUpdater } from '.';

export { PreactContext };

/**
* The type of arguments passed to a Hook function. While this type is not
* strictly necessary, they are given a type name to make it easier to read
* the following types and trace the flow of data.
*/
export type HookArgs = any;

/**
* The return type of a Hook function. While this type is not
* strictly necessary, they are given a type name to make it easier to read
* the following types and trace the flow of data.
*/
export type HookReturnValue = any;

/** The public function a user invokes to use a Hook */
export type Hook = (...args: HookArgs[]) => HookReturnValue;
export interface Options extends globalThis.Options {
/** Attach a hook that is invoked before a vnode is diffed. */
_diff?(vnode: VNode): void;
diffed?(vnode: VNode): void;
/** Attach a hook that is invoked before a vnode has rendered. */
_render?(vnode: VNode): void;
/** Attach a hook that is invoked after a tree was mounted or was updated. */
_commit?(vnode: VNode, commitQueue: Component[]): void;
_unmount?(vnode: VNode): void;
/** Attach a hook that is invoked before a hook's state is queried. */
_hook?(component: Component, index: number, type: HookType): void;
}

// Hook tracking

Expand All @@ -34,52 +24,72 @@ export interface ComponentHooks {
_pendingEffects: EffectHookState[];
}

export interface Component extends PreactComponent<any, any> {
export interface Component extends globalThis.Component<any, any> {
__hooks?: ComponentHooks;
// Extend to include HookStates
_renderCallbacks?: Array<HookState | (() => void)>;
_hasScuFromHooks?: boolean;
}

export interface VNode extends PreactVNode {
export interface VNode extends globalThis.VNode {
_mask?: [number, number];
_component?: Component; // Override with our specific Component type
}

export type HookState =
| EffectHookState
| MemoHookState
| ReducerHookState
| ContextHookState
| ErrorBoundaryHookState;
| ErrorBoundaryHookState
| IdHookState;

interface BaseHookState {
_value?: unknown;
_nextValue?: undefined;
_pendingValue?: undefined;
_args?: undefined;
_pendingArgs?: undefined;
_component?: undefined;
_cleanup?: undefined;
}

export type Effect = () => void | Cleanup;
export type Cleanup = () => void;

export interface EffectHookState {
export interface EffectHookState extends BaseHookState {
_value?: Effect;
_args?: any[];
_pendingArgs?: any[];
_args?: unknown[];
_pendingArgs?: unknown[];
_cleanup?: Cleanup | void;
}

export interface MemoHookState {
_value?: any;
_pendingValue?: any;
_args?: any[];
_pendingArgs?: any[];
_factory?: () => any;
export interface MemoHookState<T = unknown> extends BaseHookState {
_value?: T;
_pendingValue?: T;
_args?: unknown[];
_pendingArgs?: unknown[];
_factory?: () => T;
}

export interface ReducerHookState {
_nextValue?: any;
_value?: any;
export interface ReducerHookState<S = unknown, A = unknown>
extends BaseHookState {
_nextValue?: [S, StateUpdater<S>];
_value?: [S, StateUpdater<S>];
_component?: Component;
_reducer?: Reducer<any, any>;
_reducer?: Reducer<S, A>;
}

export interface ContextHookState {
export interface ContextHookState extends BaseHookState {
/** Whether this hooks as subscribed to updates yet */
_value?: boolean;
_context?: PreactContext;
}

export interface ErrorBoundaryHookState {
_value?: (error: any, errorInfo: ErrorInfo) => void;
export interface ErrorBoundaryHookState extends BaseHookState {
_value?: (error: unknown, errorInfo: ErrorInfo) => void;
}

export interface IdHookState extends BaseHookState {
_value?: string;
}
2 changes: 1 addition & 1 deletion jsconfig-lint.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "./jsconfig.json",
"include": ["src/**/*"]
"include": ["src/**/*", "hooks/src/**/*"]
}

0 comments on commit 4f2a04f

Please sign in to comment.