Skip to content

Feat/compt 32 async lifecycle hooks#9

Merged
a-elkhiraooui-ciscode merged 2 commits intodevelopfrom
feat/COMPT-32-async-lifecycle-hooks
Mar 30, 2026
Merged

Feat/compt 32 async lifecycle hooks#9
a-elkhiraooui-ciscode merged 2 commits intodevelopfrom
feat/COMPT-32-async-lifecycle-hooks

Conversation

@a-elkhiraooui-ciscode
Copy link
Copy Markdown

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

…useIsFirstRender

- usePrevious<T>(value): previous render value via state-derivation; undefined on first render
- useToggle(initial?): boolean toggle with stable useCallback reference
- useInterval(callback, delay|null): fires at cadence, stops on null, latest callback via ref
- useTimeout(callback, delay|null): fires once, cancels on null or unmount, latest callback via ref
- useIsFirstRender(): true only on first render (scoped eslint-disable for intentional ref access)
- All timer cleanup in useEffect return — StrictMode safe
- Zero runtime deps; tsc --noEmit passes, lint passes, 25/25 tests, hooks coverage >= 98%
- All five exported from src/hooks/index.ts -> src/index.ts
- Changeset added, copilot-instructions.md updated with all three COMPT groups complete
- Moved all 5 hook test files from src/hooks/ to src/hooks/__tests__/
- Updated relative imports from ./hook to ../hook
- No logic changes; all 25 tests still pass
Copilot AI review requested due to automatic review settings March 30, 2026 13:31
@a-elkhiraooui-ciscode a-elkhiraooui-ciscode merged commit 0117305 into develop Mar 30, 2026
3 of 4 checks passed
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Async & Lifecycle” set of hooks to the HooksKit package, along with tests and public exports.

Changes:

  • Introduces five new hooks: usePrevious, useToggle, useInterval, useTimeout, useIsFirstRender
  • Exports the new hooks from the hooks barrel (src/hooks/index.ts) so they’re available via src/index.ts
  • Adds Vitest coverage for the new hooks and a changeset entry for a minor release

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/hooks/useToggle.ts New boolean toggle hook with stable callback
src/hooks/useTimeout.ts New timeout hook (delay-controlled)
src/hooks/usePrevious.ts New previous-value hook
src/hooks/useIsFirstRender.ts New first-render detection hook
src/hooks/useInterval.ts New interval hook (delay-controlled)
src/hooks/index.ts Exports the newly added hooks
src/hooks/tests/useToggle.test.ts Adds tests for useToggle
src/hooks/tests/useTimeout.test.ts Adds tests for useTimeout
src/hooks/tests/usePrevious.test.ts Adds tests for usePrevious
src/hooks/tests/useIsFirstRender.test.ts Adds tests for useIsFirstRender
src/hooks/tests/useInterval.test.ts Adds tests for useInterval
.github/instructions/copilot-instructions.md Updates hook group documentation (currently contains a merge conflict marker)
.changeset/COMPT-32-async-lifecycle-hooks.md Declares a minor release and documents the new hooks

Comment on lines +1 to +15
import { useRef } from 'react';

export function useIsFirstRender(): boolean {
const isFirstRender = useRef(true);

// Reading and writing ref.current during render is intentional here:
// isFirstRender tracks mount state only and never drives output directly.
/* eslint-disable react-hooks/refs */
if (isFirstRender.current) {
isFirstRender.current = false;
return true;
}
/* eslint-enable react-hooks/refs */

return false;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook mutates ref.current during render. In React 18 StrictMode/concurrent rendering, render can be invoked multiple times before commit; this can cause the initial mount to return false on the second render pass. Prefer setting the ref in an effect (e.g., flip a mounted flag in useEffect([]) / useLayoutEffect([])) and only reading it during render.

Suggested change
import { useRef } from 'react';
export function useIsFirstRender(): boolean {
const isFirstRender = useRef(true);
// Reading and writing ref.current during render is intentional here:
// isFirstRender tracks mount state only and never drives output directly.
/* eslint-disable react-hooks/refs */
if (isFirstRender.current) {
isFirstRender.current = false;
return true;
}
/* eslint-enable react-hooks/refs */
return false;
import { useEffect, useRef } from 'react';
export function useIsFirstRender(): boolean {
const isFirstRender = useRef(true);
useEffect(() => {
isFirstRender.current = false;
}, []);
return isFirstRender.current;

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +19
useEffect(() => {
if (delay === null) return;

const id = window.setInterval(() => {
callbackRef.current();
}, delay);

return () => {
window.clearInterval(id);
};
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useInterval uses window.setInterval/window.clearInterval without a typeof window === 'undefined' guard. Other hooks in this package guard window access (e.g., src/hooks/useWindowSize.ts:16-18), and using window here will throw in non-browser runtimes (React Native, some test runners). Prefer globalThis.setInterval/clearInterval (or setInterval/clearInterval) or add a window guard for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +18
const id = window.setTimeout(() => {
callbackRef.current();
}, delay);

return () => {
window.clearTimeout(id);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useTimeout uses window.setTimeout/window.clearTimeout without a typeof window === 'undefined' guard. Other hooks in this package guard window access (e.g., src/hooks/useWindowSize.ts:16-18), and using window here will throw in non-browser runtimes. Prefer globalThis.setTimeout/clearTimeout (or setTimeout/clearTimeout) or add a window guard for consistency.

Suggested change
const id = window.setTimeout(() => {
callbackRef.current();
}, delay);
return () => {
window.clearTimeout(id);
const id = setTimeout(() => {
callbackRef.current();
}, delay);
return () => {
clearTimeout(id);

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,64 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useToggle } from '../useToggle';
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test files for existing hooks are colocated in src/hooks (e.g., src/hooks/useDebounce.test.ts, src/hooks/useWindowSize.test.ts). These new tests live under src/hooks/__tests__, which is inconsistent and makes the test layout harder to scan. Consider colocating these tests alongside the corresponding hook files for consistency.

Suggested change
import { useToggle } from '../useToggle';
import { useToggle } from './useToggle';

Copilot uses AI. Check for mistakes.
- **State & Storage** (COMPT-30 ✅) — `useDebounce`, `useLocalStorage`, `useSessionStorage`
- **DOM & Events** (COMPT-31 ✅) — `useMediaQuery`, `useWindowSize`, `useClickOutside`, `useIntersectionObserver`
- **Async & Lifecycle** — _(upcoming)_
<<<<<<< HEAD
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file contains an unresolved merge conflict marker (<<<<<<< HEAD). This will break the markdown/instructions and should be resolved (remove conflict markers and keep the intended content).

Suggested change
<<<<<<< HEAD

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +10
import { useState } from 'react';

export function usePrevious<T>(value: T): T | undefined {
const [[prev, curr], setState] = useState<[T | undefined, T]>([undefined, value]);

if (curr !== value) {
setState([curr, value]);
}

return prev;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usePrevious calls setState during render based on curr !== value. This comparison is not safe for all values (e.g., NaN !== NaN), which can trigger an infinite render loop. Consider switching to a ref+effect implementation, or at minimum use Object.is(curr, value) for the comparison to avoid NaN edge cases.

Suggested change
import { useState } from 'react';
export function usePrevious<T>(value: T): T | undefined {
const [[prev, curr], setState] = useState<[T | undefined, T]>([undefined, value]);
if (curr !== value) {
setState([curr, value]);
}
return prev;
import { useEffect, useRef } from 'react';
export function usePrevious<T>(value: T): T | undefined {
const currentRef = useRef<T | undefined>(undefined);
const previousRef = useRef<T | undefined>(undefined);
useEffect(() => {
previousRef.current = currentRef.current;
currentRef.current = value;
}, [value]);
return previousRef.current;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants