Skip to content

Commit

Permalink
feat: deps for useConditionalUpdateEffect and useConditionalEffect (
Browse files Browse the repository at this point in the history
#201)

Add new argument for above functions.

fix: 157

BREAKING CHANGE: `useConditionalUpdateEffect` and `useConditionalEffect`
now has changed call signature (new argument).
  • Loading branch information
xobotyi authored Jul 16, 2021
1 parent d328e96 commit bd56af3
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 54 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ export { useMediaQuery } from './useMediaQuery/useMediaQuery';
export { useClickOutside } from './useClickOutside/useClickOutside';
export { useDocumentTitle, IUseDocumentTitleOptions } from './useDocumentTitle/useDocumentTitle';
export { useEventListener } from './useEventListener/useEventListener';

export { truthyAndArrayPredicate, truthyOrArrayPredicate } from './util/const';
1 change: 1 addition & 0 deletions src/useConditionalEffect/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const Example: React.FC = () => {
alert('COUNTERS VALUES ARE EVEN');
},
[state1, state2],
[state1, state2],
(conditions) => conditions.every((i) => i && i % 2 === 0)
);

Expand Down
10 changes: 7 additions & 3 deletions src/useConditionalEffect/__docs__/story.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = (
function useConditionalEffect<T extends ReadonlyArray<any>>(
callback: React.EffectCallback,
conditions: T,
deps?: React.DependencyList,
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.
- **callback** _`React.EffectCallback`_ - Effect callback like for `React.useEffect` hook
- **conditions** _`ReadonlyArray<any>`_ - Conditions that are matched against predicate
- **deps** _`React.DependencyList`_ - Dependencies list like for `useEffect`. If set - effect will
be triggered when deps changed AND conditions are satisfying predicate.
- **predicate** _`IUseConditionalEffectPredicate<ReadonlyArray<any>>`_ - Predicate that matches
conditions.
By default, it is all-truthy provision, meaning that all conditions should be truthy.
39 changes: 34 additions & 5 deletions src/useConditionalEffect/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { useConditionalEffect } from '../..';
import { truthyOrArrayPredicate, useConditionalEffect } from '../..';

describe('useConditionalEffect', () => {
it('should be defined', () => {
Expand Down Expand Up @@ -36,12 +36,41 @@ describe('useConditionalEffect', () => {
expect(spy).toHaveBeenCalledTimes(0);
});

it('should apply custom predicate', () => {
it('should invoke callback only if deps are changed and conditions match 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[] },
const { rerender } = renderHook(({ cond, deps }) => useConditionalEffect(spy, cond, deps), {
initialProps: { cond: [false] as unknown[], deps: [1] as any[] },
});
expect(spy).toHaveBeenCalledTimes(0);

rerender({ cond: [false], deps: [2] });
expect(spy).toHaveBeenCalledTimes(0);

rerender({ cond: [true], deps: [2] });
expect(spy).toHaveBeenCalledTimes(0);

rerender({ cond: [true], deps: [3] });
expect(spy).toHaveBeenCalledTimes(1);

rerender({ cond: [true], deps: [3] });
expect(spy).toHaveBeenCalledTimes(1);

rerender({ cond: [true], deps: [4] });
expect(spy).toHaveBeenCalledTimes(2);

rerender({ cond: [false], deps: [5] });
expect(spy).toHaveBeenCalledTimes(2);
});

it('should apply custom predicate', () => {
const spy = jest.fn();
const predicateSpy = jest.fn((conditions) => truthyOrArrayPredicate(conditions));
const { rerender } = renderHook(
({ cond }) => useConditionalEffect(spy, cond, undefined, predicateSpy),
{
initialProps: { cond: [null] as unknown[] },
}
);
expect(predicateSpy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(0);

Expand Down
6 changes: 3 additions & 3 deletions src/useConditionalEffect/__tests__/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ describe('useConditionalEffect', () => {
expect(result.error).toBeUndefined();
});

it('should not invoke effect, but should invoke predicate', () => {
it('should not invoke nor effect nor predicate', () => {
const spy = jest.fn();
const predicateSpy = jest.fn((arr: unknown[]) => arr.some((i) => Boolean(i)));
renderHook(() => useConditionalEffect(spy, [true], predicateSpy));
expect(predicateSpy).toHaveBeenCalledTimes(1);
renderHook(() => useConditionalEffect(spy, [true], undefined, predicateSpy));
expect(predicateSpy).toHaveBeenCalledTimes(0);
expect(spy).toHaveBeenCalledTimes(0);
});
});
31 changes: 14 additions & 17 deletions src/useConditionalEffect/useConditionalEffect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EffectCallback, useEffect, useRef } from 'react';
import { noop, truthyArrayItemsPredicate } from '../util/const';
import { DependencyList, EffectCallback, useEffect } from 'react';
import { truthyAndArrayPredicate } from '..';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = (
Expand All @@ -11,26 +11,23 @@ export type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = (
*
* @param callback Callback to invoke
* @param conditions Conditions array
* @param deps Dependencies list like for `useEffect`. If set - effect will be
* triggered when deps changed AND conditions are satisfying predicate.
* @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>>(
export function useConditionalEffect<T extends ReadonlyArray<unknown>>(
callback: EffectCallback,
conditions: T,
predicate: IUseConditionalEffectPredicate<T> = truthyArrayItemsPredicate
deps?: DependencyList,
predicate: IUseConditionalEffectPredicate<T> = truthyAndArrayPredicate
): 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]);
// eslint-disable-next-line consistent-return
useEffect(() => {
if (predicate(conditions)) {
return callback();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const Example: React.FC = () => {
alert('COUNTERS VALUES ARE EVEN');
},
[state1, state2],
[state1, state2],
(conditions) => conditions.every((i) => i && i % 2 === 0)
);

Expand Down
6 changes: 5 additions & 1 deletion src/useConditionalUpdateEffect/__docs__/story.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type IUseConditionalUpdateEffectPredicate<Cond extends ReadonlyArray<any>> = (
function useConditionalUpdateEffect<T extends ReadonlyArray<any>>(
callback: React.EffectCallback,
conditions: T,
deps?: React.DependencyList,
predicate?: IUseConditionalUpdateEffectPredicate<T>
): void;
```
Expand All @@ -34,5 +35,8 @@ function useConditionalUpdateEffect<T extends ReadonlyArray<any>>(

- _**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.
- **deps** _`React.DependencyList`_ - Dependencies list like for `useEffect`. If set - effect will
be triggered when deps changed AND conditions are satisfying predicate.
- _**predicate**_ _`IUseConditionalUpdateEffectPredicate<ReadonlyArray<any>>`_ - Predicate that
matches conditions.
By default, it is all-truthy provision, meaning that all conditions should be truthy.
32 changes: 29 additions & 3 deletions src/useConditionalUpdateEffect/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { useConditionalUpdateEffect } from '../..';
import { useConditionalEffect, useConditionalUpdateEffect } from '../..';

describe('useConditionalUpdateEffect', () => {
it('should be defined', () => {
Expand Down Expand Up @@ -28,10 +28,36 @@ describe('useConditionalUpdateEffect', () => {
expect(spy).toHaveBeenCalledTimes(1);
});

it('should invoke callback only if deps are changed and conditions match predicate', () => {
const spy = jest.fn();
const { rerender } = renderHook(({ cond, deps }) => useConditionalEffect(spy, cond, deps), {
initialProps: { cond: [false] as unknown[], deps: [1] as any[] },
});
expect(spy).toHaveBeenCalledTimes(0);

rerender({ cond: [false], deps: [2] });
expect(spy).toHaveBeenCalledTimes(0);

rerender({ cond: [true], deps: [2] });
expect(spy).toHaveBeenCalledTimes(0);

rerender({ cond: [true], deps: [3] });
expect(spy).toHaveBeenCalledTimes(1);

rerender({ cond: [true], deps: [3] });
expect(spy).toHaveBeenCalledTimes(1);

rerender({ cond: [true], deps: [4] });
expect(spy).toHaveBeenCalledTimes(2);

rerender({ cond: [false], deps: [5] });
expect(spy).toHaveBeenCalledTimes(2);
});

it('nor callback neither predicate should not be called on mount', () => {
const spy = jest.fn();
const predicateSpy = jest.fn(() => true);
renderHook(() => useConditionalUpdateEffect(spy, [true], predicateSpy));
renderHook(() => useConditionalUpdateEffect(spy, [true], undefined, predicateSpy));
expect(predicateSpy).toHaveBeenCalledTimes(0);
expect(spy).toHaveBeenCalledTimes(0);
});
Expand All @@ -40,7 +66,7 @@ describe('useConditionalUpdateEffect', () => {
const spy = jest.fn();
const predicateSpy = jest.fn((arr: unknown[]) => arr.some((i) => Boolean(i)));
const { rerender } = renderHook(
({ cond }) => useConditionalUpdateEffect(spy, cond, predicateSpy),
({ cond }) => useConditionalUpdateEffect(spy, cond, undefined, predicateSpy),
{
initialProps: { cond: [null] as unknown[] },
}
Expand Down
2 changes: 1 addition & 1 deletion src/useConditionalUpdateEffect/__tests__/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('useConditionalUpdateEffect', () => {
it('nor callback neither predicate should not be called on mount', () => {
const spy = jest.fn();
const predicateSpy = jest.fn(() => true);
renderHook(() => useConditionalUpdateEffect(spy, [true], predicateSpy));
renderHook(() => useConditionalUpdateEffect(spy, [true], undefined, predicateSpy));
expect(predicateSpy).toHaveBeenCalledTimes(0);
expect(spy).toHaveBeenCalledTimes(0);
});
Expand Down
32 changes: 13 additions & 19 deletions src/useConditionalUpdateEffect/useConditionalUpdateEffect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { EffectCallback, useEffect, useRef } from 'react';
import { noop, truthyArrayItemsPredicate } from '../util/const';
import { useFirstMountState } from '..';
import { DependencyList, EffectCallback } from 'react';
import { truthyAndArrayPredicate, useUpdateEffect } from '..';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type IUseConditionalUpdateEffectPredicate<Cond extends ReadonlyArray<any>> = (
Expand All @@ -12,27 +11,22 @@ export type IUseConditionalUpdateEffectPredicate<Cond extends ReadonlyArray<any>
*
* @param callback Callback to invoke
* @param conditions Conditions array
* @param deps Dependencies list like for `useEffect`. If set - effect will be
* triggered when deps changed AND conditions are satisfying predicate.
* @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>>(
export function useConditionalUpdateEffect<T extends ReadonlyArray<unknown>>(
callback: EffectCallback,
conditions: T,
predicate: IUseConditionalUpdateEffectPredicate<T> = truthyArrayItemsPredicate
deps?: DependencyList,
predicate: IUseConditionalUpdateEffectPredicate<T> = truthyAndArrayPredicate
): 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]);
// eslint-disable-next-line consistent-return
useUpdateEffect(() => {
if (predicate(conditions)) {
return callback();
}
}, deps);
}
7 changes: 5 additions & 2 deletions src/util/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export const isBrowser =
typeof navigator !== 'undefined' &&
typeof document !== 'undefined';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function truthyArrayItemsPredicate(conditions: ReadonlyArray<any>): boolean {
export function truthyAndArrayPredicate(conditions: ReadonlyArray<unknown>): boolean {
return conditions.every((i) => Boolean(i));
}

export function truthyOrArrayPredicate(conditions: ReadonlyArray<unknown>): boolean {
return conditions.some((i) => Boolean(i));
}

0 comments on commit bd56af3

Please sign in to comment.