Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve types of hook source #4229

Merged
merged 3 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/**/*"]
}
Loading