Skip to content

Commit

Permalink
add useCooldownState hook
Browse files Browse the repository at this point in the history
  • Loading branch information
konstantin-lukas committed Oct 4, 2024
1 parent ca4f179 commit 1595b81
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 2 deletions.
19 changes: 19 additions & 0 deletions examples/app/demoUseCooldownState/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import React from 'react';
import {useCooldownState} from "@/../src";

const Page = () => {
const [state, setState, forceUpdate] = useCooldownState(true, 1000);
return (
<>
<h1>
My favorite color is: {state ? "green" : "red"}
</h1>
<button onClick={() => setState(!state)}>Change Opinion</button>
<button onClick={() => forceUpdate(!state)}>Change Opinion Immediately</button>
</>
);
};

export default Page;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anzol",
"version": "4.0.1",
"version": "4.1.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
Expand Down
41 changes: 41 additions & 0 deletions src/hooks/useCooldownState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useCallback, useState } from "react";

/**
* This hook wraps the standard useState hook but provides a way to block state updates for a provided amount of time.
* Further state updates during this cooldown time are discarded.
* @param initialState - The initialState passed to the useState hook.
* @param delay - The amount of time in milliseconds to block consecutive state updates.
* @return An array containing the read-only state (index 0), the setState function (index 1) which enforces the
* cooldown on updates, and another setState function (index 2) which forces a state update independent of the cooldown.
* @example
* ```tsx
* const Page = () => {
* const [state, setState, forceUpdate] = useCooldownState(true, 1000);
* return (
* <>
* <h1>
* My favorite color is: {state ? "green" : "red"}
* </h1>
* <button onClick={() => setState(!state)}>Change Opinion</button>
* <button onClick={() => forceUpdate(!state)}>Change Opinion Immediately</button>
* </>
* );
* };
* ```
*/

function useCooldownState<T>(initialState: T, delay: number): [T, (newValue: T) => void, (newValue: T) => void] {
const [state, setState] = useState(initialState);
const [blockUpdate, setBlockUpdate] = useState(false);

const setStateWrapper = useCallback((newState: T) => {
if (blockUpdate) return;
setState(newState);
setBlockUpdate(true);
setTimeout(() => setBlockUpdate(false), delay);
}, [blockUpdate]);

return [state, setStateWrapper, setState];
}

export default useCooldownState;
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ export {
default as useDarkMode,
type DarkModeState,
type DarkModeOptions,
} from "./hooks/useDarkMode";
} from "./hooks/useDarkMode";

export {
default as useCooldownState,
} from "./hooks/useCooldownState";
67 changes: 67 additions & 0 deletions tests/useCooldownState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { act, renderHook } from "@testing-library/react";
import { useCooldownState } from "../src";

describe("useCooldownState", () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

test("should initialize with the initial state", () => {
const { result } = renderHook(() => useCooldownState(true, 1000));
const [state] = result.current;

expect(state).toBe(true);
});

test("should update state when setState is called and block subsequent updates for the cooldown period", () => {
const { result } = renderHook(() => useCooldownState(true, 1000));

// Set state to false (should work immediately)
act(() => {
result.current[1](false);
});

expect(result.current[0]).toBe(false); // state should change

// Try to set state again before the cooldown ends (should not work)
act(() => {
result.current[1](true);
});

expect(result.current[0]).toBe(false); // state should not change

// Advance time by 1000ms to simulate cooldown completion
act(() => {
jest.advanceTimersByTime(1000);
});

// Try to set state again after cooldown (should work)
act(() => {
result.current[1](true);
});

expect(result.current[0]).toBe(true); // state should now change*/
});

test("should force update the state using forceUpdate regardless of the cooldown", () => {
const { result } = renderHook(() => useCooldownState(true, 1000));

// Set state to false using forceUpdate (should bypass cooldown)
act(() => {
result.current[2](false);
});

expect(result.current[0]).toBe(false); // state should change immediately

// Try to force update again (should work immediately)
act(() => {
result.current[2](true);
});

expect(result.current[0]).toBe(true); // state should change immediately
});
});

0 comments on commit 1595b81

Please sign in to comment.