-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/main' into #102-allow-ref-object…
…-in-use-event-listener
- Loading branch information
Showing
30 changed files
with
5,402 additions
and
4,796 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { Meta } from '@storybook/blocks'; | ||
|
||
<Meta title="hooks/lifecycle/useBeforeMount" /> | ||
|
||
# useBeforeMount | ||
|
||
**Synchronously** run a function before the component is mounted, only once. | ||
|
||
This is especially useful for initializing objects that are used in code further down. | ||
|
||
It's opposite of `useMount`, which runs asynchronously after the component is mounted. | ||
|
||
> To know when a component is rendering for the first time, use the `useIsMounted` hook, | ||
which returned boolean ref can be used inline in other component code or JSX. | ||
|
||
## Reference | ||
|
||
```ts | ||
function useBeforeMount(callback: () => void): void; | ||
``` | ||
|
||
### Parameters | ||
|
||
* `callback` - A function to be executed during the initial render, before mounting, but not | ||
during subsequent renders. | ||
|
||
## Usage | ||
|
||
```tsx | ||
function DemoComponent() { | ||
useBeforeMount(() => { | ||
console.log('I will run before the component is mounted'); | ||
}); | ||
|
||
return ( | ||
<div> | ||
</div> | ||
); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { jest } from '@jest/globals'; | ||
import { renderHook } from '@testing-library/react'; | ||
import { useEffect, useState } from 'react'; | ||
import { useMount } from '../useMount/useMount.js'; | ||
import { useBeforeMount } from './useBeforeMount.js'; | ||
|
||
describe('useBeforeMount', () => { | ||
it('should not crash', async () => { | ||
renderHook(() => { | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
useBeforeMount(() => {}); | ||
}); | ||
}); | ||
|
||
it('should execute a callback on first render', async () => { | ||
const spy = jest.fn(); | ||
renderHook(() => { | ||
useBeforeMount(spy); | ||
}); | ||
expect(spy).toBeCalledTimes(1); | ||
}); | ||
|
||
it('should execute during synchronous render, before mount', async () => { | ||
const beforeMount = jest.fn(); | ||
const inlineSpy = jest.fn(); | ||
const mountedSpy = jest.fn(); | ||
renderHook(() => { | ||
useEffect(() => { | ||
mountedSpy(); | ||
}, []); | ||
|
||
useBeforeMount(beforeMount); | ||
|
||
inlineSpy(); | ||
}); | ||
|
||
expect(beforeMount).toBeCalledTimes(1); | ||
expect(mountedSpy).toBeCalledTimes(1); | ||
expect(inlineSpy).toBeCalledTimes(1); | ||
expect(beforeMount.mock.invocationCallOrder[0]).toBeLessThan( | ||
mountedSpy.mock.invocationCallOrder[0], | ||
); | ||
expect(beforeMount.mock.invocationCallOrder[0]).toBeLessThan( | ||
inlineSpy.mock.invocationCallOrder[0], | ||
); | ||
}); | ||
|
||
it('should not execute a callback on re-renders', async () => { | ||
const spy = jest.fn(); | ||
const { rerender } = renderHook(() => { | ||
useBeforeMount(spy); | ||
}); | ||
expect(spy).toBeCalledTimes(1); | ||
|
||
await Promise.resolve(); | ||
rerender(); | ||
expect(spy).toBeCalledTimes(1); | ||
|
||
await Promise.resolve(); | ||
rerender(); | ||
expect(spy).toBeCalledTimes(1); | ||
}); | ||
|
||
it('should only execute once when setState is called during useMount', async () => { | ||
const spy = jest.fn(); | ||
const { rerender } = renderHook(() => { | ||
// eslint-disable-next-line react/hook-use-state | ||
const [, setState] = useState(false); | ||
|
||
useBeforeMount(spy); | ||
|
||
useMount(() => { | ||
setState(true); | ||
}); | ||
}); | ||
expect(spy).toBeCalledTimes(1); | ||
|
||
await Promise.resolve(); | ||
rerender(); | ||
expect(spy).toBeCalledTimes(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useRef } from 'react'; | ||
import { useMount } from '../useMount/useMount.js'; | ||
|
||
/** | ||
* Executes a callback during the initial render, before mounting, but not during subsequent | ||
* renders. | ||
* | ||
* Opposed to `useEffect` / `useMount`, the callback is executed synchronously, | ||
* before the component is mounted. | ||
* | ||
* @param callback A function to be executed during the initial render, before mounting, but not | ||
* during subsequent renders. | ||
*/ | ||
export function useBeforeMount(callback: () => void): void { | ||
const isBeforeMount = useRef(true); | ||
|
||
if (isBeforeMount.current) { | ||
callback(); | ||
} | ||
|
||
useMount(() => { | ||
isBeforeMount.current = false; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Meta } from '@storybook/blocks'; | ||
|
||
<Meta title="hooks/lifecycle/useForceRerender" /> | ||
|
||
# useForceRerender | ||
|
||
Forces a rerender of the component when the returned function is called. | ||
|
||
This should only be used when there is no other state that can be used to trigger a rerender. | ||
|
||
Examples could be to force a rerender after a timeout or interval, to compare the current time | ||
(that changes) with the time when the component was last updated. | ||
|
||
## Reference | ||
|
||
```ts | ||
function useForceRerender(): () => void; | ||
``` | ||
|
||
### Returns | ||
|
||
* `forceRerender` - A function that can be called to force a rerender. | ||
|
||
## Usage | ||
|
||
```ts | ||
const forceRerender = useForceRerender(); | ||
|
||
forceRerender(); | ||
``` | ||
|
||
```tsx | ||
function DemoComponent() { | ||
const forceRerender = useForceRerender(); | ||
|
||
return ( | ||
<div> | ||
<div>Time: {Date.now()}</div> | ||
<button onClick={forceRerender}>Update</button> | ||
</div> | ||
); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* eslint-disable react/jsx-no-literals,react/jsx-handler-names */ | ||
import type { StoryObj } from '@storybook/react'; | ||
import { useForceRerender } from './useForceRerender.js'; | ||
|
||
export default { | ||
title: 'hooks/lifecycle/useForceRerender', | ||
}; | ||
|
||
function DemoComponent(): JSX.Element { | ||
const forceRerender = useForceRerender(); | ||
|
||
return ( | ||
<div> | ||
<div className="alert alert-primary"> | ||
<h4 className="alert-heading">Instructions!</h4> | ||
<p className="mb-0">Click the "Update" button, and notice the date updating.</p> | ||
</div> | ||
<div className="card border-dark" data-ref="test-area"> | ||
<div className="card-header">Test Area</div> | ||
<div className="card-body"> | ||
<p>{Date.now()}</p> | ||
<button type="button" className="btn btn-primary" onClick={forceRerender}> | ||
Update | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export const Demo: StoryObj = { | ||
render() { | ||
return <DemoComponent />; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { jest } from '@jest/globals'; | ||
import { act, renderHook } from '@testing-library/react'; | ||
import { useForceRerender } from './useForceRerender.js'; | ||
|
||
describe('useForceRerender', () => { | ||
it('should not crash', async () => { | ||
renderHook(() => useForceRerender()); | ||
}); | ||
|
||
it('should return a function', async () => { | ||
const { | ||
result: { current: forceRerender }, | ||
} = renderHook(() => useForceRerender()); | ||
|
||
expect(forceRerender).toBeInstanceOf(Function); | ||
}); | ||
|
||
it('should force a rerender', async () => { | ||
const spy = jest.fn(); | ||
const { | ||
result: { current: forceRerender }, | ||
} = renderHook(() => { | ||
// eslint-disable-next-line @typescript-eslint/no-shadow | ||
const forceRerender = useForceRerender(); | ||
|
||
spy(); | ||
|
||
return forceRerender; | ||
}); | ||
|
||
expect(spy).toBeCalledTimes(1); | ||
|
||
await act(() => { | ||
forceRerender(); | ||
}); | ||
|
||
expect(spy).toBeCalledTimes(2); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { useReducer } from 'react'; | ||
|
||
// use an arbitrary large number instead of a toggle boolean to avoid potential optimization issues | ||
// when called multiple times in a single render, but have a cap to avoid overflow | ||
const updateReducer = (value: number): number => (value + 1) % Number.MAX_SAFE_INTEGER; | ||
|
||
/** | ||
* Forces a rerender of the component when the returned function is called. | ||
* | ||
* This should only be used when there is no other state that can be used to trigger a rerender. | ||
* | ||
* Examples could be to force a rerender after a timeout or interval, to compare the current time | ||
* (that changes) with the time when the component was last updated. | ||
* | ||
* @returns A function that can be called to force a rerender. | ||
*/ | ||
export function useForceRerender(): () => void { | ||
const [, update] = useReducer(updateReducer, 0); | ||
|
||
return update; | ||
} |
Oops, something went wrong.