Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ npm install usable-react
- [`useDeferredChildren`](./docs/use-deferred-children.md)
- [`useDomEvent`](./docs/use-dom-event.md)
- [`useEffectAfterMount`](./docs/use-effect-after-mount.md)
- [`useEffectOnce`](./docs/use-effect-once.md)
- [`useEffectTrigger`](./docs/use-effect-trigger.md)
- [`useFilter`](./docs/use-filter.md)
- [`useForceUpdate`](./docs/use-force-update.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/use-effect-after-mount.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `useEffectAfterMount`

Same as React's native `useEffect`, except that it will skip the initial render.
Same as React's `useEffect`, except that it will skip the initial render.

## Example

Expand Down
11 changes: 11 additions & 0 deletions docs/use-effect-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `useEffectOnce`

Same as React's `useEffect`, except that it will only run on the the initial render (even in React >=v18).

## Example

```tsx
useEffectOnce(() => {
console.log('doot doot doot!')
});
```
2 changes: 1 addition & 1 deletion docs/use-effect-trigger.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `useEffectTrigger`

Registers a side effect with the same signature as React's native `useEffect`, but the effect will only execute when the manual trigger function returned by this hook is invoked.
Registers a side effect with the same signature as React's `useEffect`, but the effect will only execute when the manual trigger function returned by this hook is invoked.

A few use-cases for this hook include:

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/use-debounced/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';

/**
* Debounces the given value. If `delay` is zero, the value is updated
* synchronously.
* Debounces the given value.
* If `delay` is zero, the value is updated synchronously.
*/
export function useDebounced<T>(value: T, delay = 300) {
const isSynchronous = delay === 0;
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/use-effect-once/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EffectCallback, useEffect, useRef } from 'react';
import { useForceUpdate } from '../use-force-update';

/**
* Exactly like `useEffect`, except that the effect only once
* (even in React >=v18 strict mode).
*
* Based on this workaround from AG Grid:
* https://blog.ag-grid.com/avoiding-react-18-double-mount/
*/
export function useEffectOnce(effect: EffectCallback) {
const destroyFunc = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const renderAfterCalled = useRef(false);
const forceUpdate = useForceUpdate();

if (effectCalled.current) {
renderAfterCalled.current = true;
}

useEffect(() => {
// Only execute the effect first time around...
if (!effectCalled.current) {
destroyFunc.current = effect();
effectCalled.current = true;
}

// Force one render after the effect is run...
forceUpdate();

return () => {
// If the comp didn't render since the useEffect
// was called, we know it's the dummy React cycle
if (!renderAfterCalled.current) return;
destroyFunc.current?.();
};
}, []);
}
5 changes: 3 additions & 2 deletions src/hooks/use-initial-render/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useEffect, useRef } from 'react';
import { useEffectOnce } from '../use-effect-once';

/**
* Returns a `boolean` indicating whether the current update is the intial
* render (when the component is first mounted).
*/
export function useInitialRender() {
const isInitialRender = useRef(true);
useEffect(() => {
useEffectOnce(() => {
isInitialRender.current = false;
}, []);
});
return isInitialRender.current;
}
5 changes: 3 additions & 2 deletions src/hooks/use-is-mounted/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useRef, useEffect, useCallback } from 'react';
import { useEffectOnce } from '../use-effect-once';

/**
* Returns a memoized callback that when invoked returns `boolean` indicating if
Expand All @@ -7,13 +8,13 @@ import { useRef, useEffect, useCallback } from 'react';
export function useIsMounted() {
const isMountedRef = useRef(false);

useEffect(() => {
useEffectOnce(() => {
isMountedRef.current = true;

return () => {
isMountedRef.current = false;
};
}, []);
});

return useCallback(() => isMountedRef.current, []);
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './hooks/use-debounced-effect';
export * from './hooks/use-deferred-children';
export * from './hooks/use-dom-event';
export * from './hooks/use-effect-after-mount';
export * from './hooks/use-effect-once';
export * from './hooks/use-effect-trigger';
export * from './hooks/use-filter';
export * from './hooks/use-force-update';
Expand Down