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

✨ [Feature] :added new hooks useHash, useHashHistory, useClipboard and useOrigin #488

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions packages/usehooks-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ export * from './useToggle'
export * from './useUnmount'
export * from './useUpdateEffect'
export * from './useWindowSize'
export * from './useClipboard'
export * from './useHash'
export * from './useOrigin'
export * from './useHashHistory'
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useClipboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useClipboard'
25 changes: 25 additions & 0 deletions packages/usehooks-ts/src/useClipboard/useClipboard.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useClipboard } from './useClipboard'

// mock message
const message = {
content: 'I am comming from openAi response',
}

export default function CopyToClipboard({ ...props }) {
const { isCopied, copyToClipboard } = useClipboard({ timeout: 1000 })

return (
<div {...props}>
<button
className="h-8 w-8 bg-gray-200 rounded-full flex items-center justify-center"
onClick={() => copyToClipboard(message.content)}
>
{isCopied ? (
<span className="text-green-500">Copied</span>
) : (
<span className="text-gray-600">Copy</span>
)}
</button>
</div>
)
}
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useClipboard/useClipboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The useClipboard hook is a custom React hook that facilitates copying text to the clipboard with optional timeout functionality.
23 changes: 23 additions & 0 deletions packages/usehooks-ts/src/useClipboard/useClipboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { act, renderHook } from '@testing-library/react';
import { useClipboard } from './useClipboard';


describe('useClipboard()', () => {
it('should copy value to clipboard and reset isCopied state after timeout', async () => {
const { result } = renderHook(() => useClipboard({ timeout: 2000 }));

act(() => {
result.current.copyToClipboard('test value');
});

// The isCopied state should be true immediately after copying
expect(result.current.isCopied).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

This wouldn't work as clipboard.writeText is an async function and you are setting the value of isCopied in the then callback. So the value doesn't update immediately


// Fast-forward time by 2000 milliseconds
await new Promise(resolve => setTimeout(resolve, 2000));

Comment on lines +15 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that works when you are fast forwarding time in tests, you should use vitest.useFakeTimers() at the start of test, then vitest.advanceTimersByTime(delay) where you want to forward the time

// The isCopied state should be reset to false after the timeout
expect(result.current.isCopied).toBe(false);
});
});

20 changes: 20 additions & 0 deletions packages/usehooks-ts/src/useClipboard/useClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useState } from 'react'

export function useClipboard({ timeout = 2000 }: { timeout?: number }) {
const [isCopied, setIsCopied] = useState<Boolean>(false)

const copyToClipboard = (value: string) => {
if (!value) return
if (typeof window === 'undefined' || !navigator.clipboard?.writeText) return

navigator.clipboard.writeText(value).then(() => {
setIsCopied(true)

setTimeout(() => {
setIsCopied(false)
}, timeout)
})
}

return { isCopied, copyToClipboard }
}
Comment on lines +3 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need another useClipboard while useCopyToClipboard exists, If you want the functionallity of setting isCopied and restting it after timeout you can do that by yourself outside of the hook. Or add that feature to the existing hook.
Don't create duplicate hooks as they tend to be confusing.

1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useHash/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useHash'
14 changes: 14 additions & 0 deletions packages/usehooks-ts/src/useHash/useHash.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useHash } from './useHash'

function Component() {
const hash = useHash();

return (
<div>
<h1>Hash String: {hash} </h1>
{/* Your component JSX */}
</div>
);
}

export default Component;
3 changes: 3 additions & 0 deletions packages/usehooks-ts/src/useHash/useHash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
We use the useHash hook to read the entire hash string from the URL.

- [`useHashHistory`](/react-hook/use-hash-history): The useHashHistory hook is used to track changes in the URL hash over time. It returns an array containing the history of hash values, starting from the initial value when the component mounts.
21 changes: 21 additions & 0 deletions packages/usehooks-ts/src/useHash/useHash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { act, renderHook } from '@testing-library/react';

import { useHash } from './useHash';


describe('useHash()', () => {
it('should update hash value on hashchange event', () => {
const { result } = renderHook(() => useHash());

// Simulate a hashchange event with a new hash value
const newHash = "newHashValue";
act(() => {
window.location.hash = `#${newHash}`;
const event = new Event('hashchange');
window.dispatchEvent(event);
});

// Check if the hash value has been updated
expect(result.current).toBe(newHash);
});
});
21 changes: 21 additions & 0 deletions packages/usehooks-ts/src/useHash/useHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useState,useEffect } from 'react';


export function useHash() {
const [hash, setHash] = useState<string>("");

useEffect(() => {
const handleHashChange = () => {
setHash(window.location.hash.slice(1));
};
handleHashChange();

window.addEventListener("hashchange", handleHashChange);

return () => {
window.removeEventListener("hashchange", handleHashChange);
};
}, []);

Choose a reason for hiding this comment

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

If I modify the code so that it renders anew every time the value of the hash changes, will this cause problems?

Copy link
Author

Choose a reason for hiding this comment

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

Modifying the code to re-render on hash changes shouldn't inherently cause problems. It's done to ensure the component reacts to hash value changes accurately.


return hash;
}
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useHashHistory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useHashHistory'
20 changes: 20 additions & 0 deletions packages/usehooks-ts/src/useHashHistory/useHashHistory.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import { useHashHistory } from "./useHashHistory";


function Component() {
const hashHistory = useHashHistory();

return (
<div>
<h1>Hash History:</h1>
<ul>
{hashHistory.map((hash, index) => (
<li key={index}>{hash}</li>
))}
</ul>
</div>
);
}

export default Component;
5 changes: 5 additions & 0 deletions packages/usehooks-ts/src/useHashHistory/useHashHistory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The useHashHistory hook is used to track changes in the URL hash over time. It returns an array containing the history of hash values, starting from the initial value when the component mounts. You can display this history in your component to show how the hash has changed during the user's interaction with the page.

### Related hooks

- [`useHash`](/react-hook/use-hash): We use the useHash hook to read the entire hash string from the URL.
43 changes: 43 additions & 0 deletions packages/usehooks-ts/src/useHashHistory/useHashHistory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { renderHook } from '@testing-library/react';
import { useHashHistory } from './useHashHistory';

describe('useHashHistory()', () => {
let originalHash: string;
let originalRemoveEventListener: any;

beforeEach(() => {
originalHash = window.location.hash;
originalRemoveEventListener = window.removeEventListener;

Comment on lines +9 to +11
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you wanted to assign vi.fn() to window.removeEventListener, as you are testing later if removeEventListener was called or not. But it would fail as removeEventListner is not a spy.

});

afterEach(() => {
window.location.hash = originalHash;
window.removeEventListener = originalRemoveEventListener; // Restore removeEventListener
});

it('should initialize with the current hash', () => {
const { result } = renderHook(() => useHashHistory());

expect(result.current).toEqual([originalHash]);
});

it('should update history array when hash changes', () => {
const { result } = renderHook(() => useHashHistory());

// Simulate a hashchange event with a new hash value
const newHash = "#newHash";
window.location.hash = newHash;
const event = new Event('hashchange');
window.dispatchEvent(event);
Comment on lines +28 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

You might want to call it inside act(()=>{/* */})


expect(result.current).toEqual([originalHash, newHash]);
});

it('should remove event listener on unmount', () => {
const { unmount } = renderHook(() => useHashHistory());
unmount();

expect(window.removeEventListener).toHaveBeenCalledWith('hashchange', expect.any(Function));
});
});
20 changes: 20 additions & 0 deletions packages/usehooks-ts/src/useHashHistory/useHashHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useState,useEffect } from 'react';

// Hook to track hash history
export function useHashHistory(): string[] {
const [history, setHistory] = useState<string[]>([window.location.hash]);
Copy link
Contributor

Choose a reason for hiding this comment

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

This hook is not SSR safe, window wouldn't be defined on server. Check other examples in the repo to learn how to make it SSR safe


useEffect(() => {
const handleHashChange = () => {
setHistory((prevHistory) => [...prevHistory, window.location.hash]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use spread operator like that, its not a good practice. Use prevHistory.concat()

};

window.addEventListener("hashchange", handleHashChange);

return () => {
window.removeEventListener("hashchange", handleHashChange);
};
}, []);

return history;
}
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useOrigin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useOrigin'
49 changes: 49 additions & 0 deletions packages/usehooks-ts/src/useOrigin/useOrigin.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import { useOrigin } from './useOrigin'

interface LiveClockProps {}

interface Time {
hours: number;
minutes: number;
seconds: number;
}

function formatTime(date: Date): Time {
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
return { hours, minutes, seconds };
}

function LiveClock(props: LiveClockProps) {
const origin = useOrigin();
const [currentTime, setCurrentTime] = useState<Date>(new Date());

useEffect(() => {
const updateCurrentTime = () => {
setCurrentTime(new Date());
};

// Update the current time every second
const intervalId = setInterval(updateCurrentTime, 1000);

return () => {
clearInterval(intervalId);
};
}, []); // Run the effect only once on component mount

const { hours, minutes, seconds } = formatTime(currentTime);

return (
<div className="bg-gray-100 p-8 rounded-md shadow-md">
<h2 className="text-2xl font-bold mb-4">Live Clock</h2>
<p className="text-gray-600 mb-2">Current Origin: {origin}</p>
<p className="text-3xl font-bold text-blue-600">
Current Time: {`${hours}:${minutes}:${seconds}`}
</p>
Comment on lines +4 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the clock has to do With useOrigin?

</div>
);
}

export default LiveClock;
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useOrigin/useOrigin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The useCurrentOrigin hook is a custom React hook designed to provide the origin (protocol, hostname, and port) of the current window location. This can be particularly useful in scenarios where real-time information is needed based on the user's context.
43 changes: 43 additions & 0 deletions packages/usehooks-ts/src/useOrigin/useOrigin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { act, renderHook } from '@testing-library/react';
import { useOrigin } from './useOrigin';

describe('useOrigin()', () => {
it('should return empty string if window.location.origin is not available', () => {
const originalLocation = window.location;
delete (window as any).location;
window.location = { origin: '' }as unknown as Location;

const { result } = renderHook(() => useOrigin());

expect(result.current).toBe('');
Comment on lines +5 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

You want to test if window.location.origin is not available, you delete window.location but then you are setting origin to "" yourself. Then what is the point of doing the test?


window.location = originalLocation;
});

it('should return empty string before component has mounted', () => {
const { result } = renderHook(() => useOrigin());

expect(result.current).toBe('');
});
Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I know, renderHook mounts the component. so the test doesn't make sense. Also Origin is never "", for the sake of test the origin is supposed to be http://localhost:3000

Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I know, renderHook mounts the component. so the test doesn't make sense. Also Origin is never "", for the sake of test the origin is supposed to be http://localhost:3000

Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I know, renderHook mounts the component. so the test doesn't make sense. Also Origin is never "", for the sake of test the origin is supposed to be http://localhost:3000


it('should return window.location.origin after component has mounted', async () => {
const originalLocation = window.location;
delete (window as any).location;
window.location = { origin: 'https://usehooks-ts.com' } as unknown as Location;

let result: any;
await act(async () => {
result = renderHook(() => useOrigin()).result;
});

expect(result.current).toBe('');

await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0)); // Waiting for the next microtask
});

expect(result.current).toBe('https://usehooks-ts.com');

window.location = originalLocation;
});
Comment on lines +23 to +42
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole test doesn't make any sense. Also I don't want to point out same things about fakeTimers as I have mentioned them previously.

});
18 changes: 18 additions & 0 deletions packages/usehooks-ts/src/useOrigin/useOrigin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useState ,useEffect} from 'react';


export const useOrigin = () => {
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";

if (!mounted) {
return "";
}

return origin;
Comment on lines +4 to +17
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need a mounted when you are setting origin to "" based on typeof window !== "undefined"

Copy link
Author

Choose a reason for hiding this comment

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

Got it thanks for feedback. Will fix it soon

}