-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new hooks, useConditionalEffect and useConditionalUpdateEffect (#…
…26) feat: new hooks, useConditionalEffect and useConditionalUpdateEffect
- Loading branch information
Showing
16 changed files
with
426 additions
and
16 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
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,36 @@ | ||
import { EffectCallback, useEffect, useRef } from 'react'; | ||
import { noop, truthyArrayItemsPredicate } from './util/const'; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = ( | ||
conditions: Cond | ||
) => boolean; | ||
|
||
/** | ||
* Like `useEffect` but callback invoked only if conditions match predicate. | ||
* | ||
* @param callback Callback to invoke | ||
* @param conditions Conditions array | ||
* @param predicate Predicate that defines whether conditions satisfying certain | ||
* provision. By default, it is all-truthy provision, meaning that all | ||
* conditions should be truthy. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function useConditionalEffect<T extends ReadonlyArray<any>>( | ||
callback: EffectCallback, | ||
conditions: T, | ||
predicate: IUseConditionalEffectPredicate<T> = truthyArrayItemsPredicate | ||
): void { | ||
const shouldInvoke = predicate(conditions); | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
const deps = useRef<{}>(); | ||
|
||
// we want callback invocation only in case all conditions matches predicate | ||
if (shouldInvoke) { | ||
deps.current = {}; | ||
} | ||
|
||
// we can't avoid on-mount invocations so slip noop callback for the cases we dont need invocation | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
useEffect(shouldInvoke ? callback : noop, [deps.current]); | ||
} |
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,38 @@ | ||
import { EffectCallback, useEffect, useRef } from 'react'; | ||
import { noop, truthyArrayItemsPredicate } from './util/const'; | ||
import { useFirstMountState } from './useFirstMountState'; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export type IUseConditionalUpdateEffectPredicate<Cond extends ReadonlyArray<any>> = ( | ||
conditions: Cond | ||
) => boolean; | ||
|
||
/** | ||
* Like `useUpdateEffect` but callback invoked only if conditions match predicate. | ||
* | ||
* @param callback Callback to invoke | ||
* @param conditions Conditions array | ||
* @param predicate Predicate that defines whether conditions satisfying certain | ||
* provision. By default, it is all-truthy provision, meaning that all | ||
* conditions should be truthy. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function useConditionalUpdateEffect<T extends ReadonlyArray<any>>( | ||
callback: EffectCallback, | ||
conditions: T, | ||
predicate: IUseConditionalUpdateEffectPredicate<T> = truthyArrayItemsPredicate | ||
): void { | ||
const isFirstMount = useFirstMountState(); | ||
const shouldInvoke = !isFirstMount && predicate(conditions); | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
const deps = useRef<{}>(); | ||
|
||
// we want callback invocation only in case all conditions matches predicate | ||
if (shouldInvoke) { | ||
deps.current = {}; | ||
} | ||
|
||
// we can't avoid on-mount invocations so slip noop callback for the cases we dont need invocation | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
useEffect(shouldInvoke ? callback : noop, [deps.current]); | ||
} |
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,51 @@ | ||
import * as React from 'react'; | ||
import { useState } from 'react'; | ||
import { useConditionalEffect, useToggle } from '../src'; | ||
|
||
export const Example: React.FC = () => { | ||
const [isToggled, toggle] = useToggle(false); | ||
|
||
const ToggledComponent: React.FC = () => { | ||
const [state1, setState1] = useState(2); | ||
const [state2, setState2] = useState(2); | ||
|
||
useConditionalEffect( | ||
() => { | ||
// eslint-disable-next-line no-alert | ||
alert('COUNTERS VALUES ARE EVEN'); | ||
}, | ||
[state1, state2], | ||
(conditions) => conditions.every((i) => i && i % 2 === 0) | ||
); | ||
|
||
return ( | ||
<div> | ||
<div>Alert will be displayed when both counters values are even</div> | ||
<div>Effect also invoked on initial mount</div> | ||
<button | ||
onClick={() => { | ||
setState1((i) => i + 1); | ||
}}> | ||
increment counter 1 [{state1}] | ||
</button>{' '} | ||
<button | ||
onClick={() => { | ||
setState2((i) => i + 1); | ||
}}> | ||
increment counter 2 [{state2}] | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
if (isToggled) { | ||
return <ToggledComponent />; | ||
} | ||
|
||
return ( | ||
<div> | ||
<div>As example component displays alert right from mount - it is initially unmounted.</div> | ||
<button onClick={() => toggle()}>Mount example component</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,38 @@ | ||
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks"; | ||
import { Example } from "./useConditionalEffect.stories"; | ||
|
||
<Meta title="Lifecycle/useConditionalEffect" component={Example} /> | ||
|
||
# useConditionalEffect | ||
|
||
Like `useEffect` but callback invoked only if conditions match predicate. By default, predicate | ||
matches if all conditions are truthy. | ||
|
||
Like `useEffect`, it performs conditions match on first mount. | ||
|
||
#### Example | ||
|
||
<Canvas> | ||
<Story story={Example} inline /> | ||
</Canvas> | ||
|
||
## Reference | ||
|
||
```ts | ||
type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = ( | ||
conditions: Cond | ||
) => boolean; | ||
|
||
function useConditionalEffect<T extends ReadonlyArray<any>>( | ||
callback: React.EffectCallback, | ||
conditions: T, | ||
predicate?: IUseConditionalEffectPredicate<T> | ||
): void; | ||
``` | ||
|
||
#### Arguments | ||
|
||
- _**callback**_ _`React.EffectCallback`_ - Effect callback like for `React.useEffect` hook | ||
- _**conditions**_ _`ReadonlyArray<any>`_ - Conditions that are matched against predicate | ||
- _**predicate**_ _`IUseConditionalEffectPredicate<ReadonlyArray<any>>`_ - Predicate that matches conditions. | ||
By default, it is all-truthy provision, meaning that all conditions should be truthy. |
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,36 @@ | ||
import * as React from 'react'; | ||
import { useState } from 'react'; | ||
import { useConditionalUpdateEffect } from '../src'; | ||
|
||
export const Example: React.FC = () => { | ||
const [state1, setState1] = useState(2); | ||
const [state2, setState2] = useState(2); | ||
|
||
useConditionalUpdateEffect( | ||
() => { | ||
// eslint-disable-next-line no-alert | ||
alert('COUNTERS VALUES ARE EVEN'); | ||
}, | ||
[state1, state2], | ||
(conditions) => conditions.every((i) => i && i % 2 === 0) | ||
); | ||
|
||
return ( | ||
<div> | ||
<div>Alert will be displayed when both counters values are even</div> | ||
<div>But effect not invoked on initial mount</div> | ||
<button | ||
onClick={() => { | ||
setState1((i) => i + 1); | ||
}}> | ||
increment counter 1 [{state1}] | ||
</button>{' '} | ||
<button | ||
onClick={() => { | ||
setState2((i) => i + 1); | ||
}}> | ||
increment counter 2 [{state2}] | ||
</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,38 @@ | ||
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks"; | ||
import { Example } from "./useConditionalUpdateEffect.stories"; | ||
|
||
<Meta title="Lifecycle/useConditionalUpdateEffect" component={Example} /> | ||
|
||
# useConditionalUpdateEffect | ||
|
||
Like `useUpdateEffect` but callback invoked only if conditions match predicate. By default, predicate | ||
matches if all conditions are truthy. | ||
|
||
Like `useUpdateEffect`, it _does not_ perform conditions match on first mount. | ||
|
||
#### Example | ||
|
||
<Canvas> | ||
<Story story={Example} inline /> | ||
</Canvas> | ||
|
||
## Reference | ||
|
||
```ts | ||
type IUseConditionalUpdateEffectPredicate<Cond extends ReadonlyArray<any>> = ( | ||
conditions: Cond | ||
) => boolean; | ||
|
||
function useConditionalUpdateEffect<T extends ReadonlyArray<any>>( | ||
callback: React.EffectCallback, | ||
conditions: T, | ||
predicate?: IUseConditionalUpdateEffectPredicate<T> | ||
): void; | ||
``` | ||
|
||
#### Arguments | ||
|
||
- _**callback**_ _`React.EffectCallback`_ - Effect callback like for `React.useEffect` hook | ||
- _**conditions**_ _`ReadonlyArray<any>`_ - Conditions that are matched against predicate | ||
- _**predicate**_ _`IUseConditionalUpdateEffectPredicate<ReadonlyArray<any>>`_ - Predicate that matches conditions. | ||
By default, it is all-truthy provision, meaning that all conditions should be truthy. |
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,59 @@ | ||
import { renderHook } from '@testing-library/react-hooks/dom'; | ||
import { useConditionalEffect } from '../../src'; | ||
|
||
describe('useConditionalEffect', () => { | ||
it('should be defined', () => { | ||
expect(useConditionalEffect).toBeDefined(); | ||
}); | ||
|
||
it('should render', () => { | ||
renderHook(() => useConditionalEffect(() => {}, [])); | ||
}); | ||
|
||
it('by default should invoke effect only in case all conditions are truthy', () => { | ||
const spy = jest.fn(); | ||
const { rerender } = renderHook(({ cond }) => useConditionalEffect(spy, cond), { | ||
initialProps: { cond: [1] as unknown[] }, | ||
}); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
rerender({ cond: [0, 1, 1] }); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
rerender({ cond: [1, {}, null] }); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
rerender({ cond: [true, {}, [], 25] }); | ||
expect(spy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('should not be called on mount if conditions are falsy', () => { | ||
const spy = jest.fn(); | ||
renderHook(({ cond }) => useConditionalEffect(spy, cond), { | ||
initialProps: { cond: [null] as unknown[] }, | ||
}); | ||
expect(spy).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('should apply custom predicate', () => { | ||
const spy = jest.fn(); | ||
const predicateSpy = jest.fn((arr: unknown[]) => arr.some((i) => Boolean(i))); | ||
const { rerender } = renderHook(({ cond }) => useConditionalEffect(spy, cond, predicateSpy), { | ||
initialProps: { cond: [null] as unknown[] }, | ||
}); | ||
expect(predicateSpy).toHaveBeenCalledTimes(1); | ||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
rerender({ cond: [true, {}, [], 25] }); | ||
expect(predicateSpy).toHaveBeenCalledTimes(2); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
rerender({ cond: [true, false, 0, null] }); | ||
expect(predicateSpy).toHaveBeenCalledTimes(3); | ||
expect(spy).toHaveBeenCalledTimes(2); | ||
|
||
rerender({ cond: [undefined, false, 0, null] }); | ||
expect(predicateSpy).toHaveBeenCalledTimes(4); | ||
expect(spy).toHaveBeenCalledTimes(2); | ||
}); | ||
}); |
Oops, something went wrong.