Skip to content

Commit

Permalink
feat: improve useConditionalEffect and remove `useConditionalUpdate…
Browse files Browse the repository at this point in the history
…Effect` (#345)

* feat: align `useConditionalEffect` signature with `useEffect`

BREAKING CHANGE: `useConditionalEffect` conditions and deps arguments now switched places.

* feat(useConditionalEffect): added ability to wrap other effect hooks

* feat: remove `useConditionalUpdateEffect`

BREAKING CHANGE: `useConditionalUpdateEffect` removed in favor of
composition with `useConditionalEffect`.

Now you should simpy pass extra argument to achieve same functionality:
`useConditionalEffect(()=>{}, undefined, [], truthyAndArrayPredicate,
useUpdateEffect)`

BREAKING CHANGE: Interface `IUseConditionalEffectPredicate` renamed to
`IConditionsPredicate`

* docs: cleanup, remove `useConditionalUpdateEffect` from readme

Co-authored-by: Joe Duncko <[email protected]>
  • Loading branch information
xobotyi and JoeDuncko authored Sep 28, 2021
1 parent eeb2871 commit 4474cf7
Show file tree
Hide file tree
Showing 15 changed files with 130 additions and 286 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ our [migration guide](https://react-hookz.github.io/web/?path=/docs/migrating-fr

- [**`useConditionalEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-useconditionaleffect--example)
— Like `useEffect` but callback invoked only if conditions match predicate.
- [**`useConditionalUpdateEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-useconditionalupdateeffect--example)
— Like `useUpdateEffect` but callback invoked only if conditions match predicate.
- [**`useDebouncedEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usedebouncedeffect--example)
— Like `useEffect`, but passed function is debounced.
- [**`useFirstMountState`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usefirstmountstate--example)
Expand Down
9 changes: 4 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ export { useThrottledCallback } from './useThrottledCallback/useThrottledCallbac
// Livecycle
export {
useConditionalEffect,
IUseConditionalEffectPredicate,
IConditionsPredicate,
IConditionsList,
} from './useConditionalEffect/useConditionalEffect';
export {
useConditionalUpdateEffect,
IUseConditionalUpdateEffectPredicate,
} from './useConditionalUpdateEffect/useConditionalUpdateEffect';
export { useDebouncedEffect } from './useDebouncedEffect/useDebouncedEffect';
export { useFirstMountState } from './useFirstMountState/useFirstMountState';
export { useIsMounted } from './useIsMounted/useIsMounted';
Expand Down Expand Up @@ -93,3 +90,5 @@ export { useDocumentTitle, IUseDocumentTitleOptions } from './useDocumentTitle/u
export { useEventListener } from './useEventListener/useEventListener';

export { truthyAndArrayPredicate, truthyOrArrayPredicate } from './util/const';

export { IEffectCallback, IEffectHook } from './util/misc';
5 changes: 3 additions & 2 deletions src/useConditionalEffect/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { useState } from 'react';
import { useConditionalEffect, useToggle } from '../..';
import { useConditionalEffect, useToggle, useUpdateEffect } from '../..';

export const Example: React.FC = () => {
const [isToggled, toggle] = useToggle(false);
Expand All @@ -16,7 +16,8 @@ export const Example: React.FC = () => {
},
[state1, state2],
[state1, state2],
(conditions) => conditions.every((i) => i && i % 2 === 0)
(conditions) => conditions.every((i) => i && i % 2 === 0),
useUpdateEffect
);

return (
Expand Down
42 changes: 26 additions & 16 deletions src/useConditionalEffect/__docs__/story.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { Example } from './example.stories';
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.
It also can make any effect hook that is compliant with `useEffect`'s call signature conditional by passing
`effectHook` parameter. This way you can implement almost any effect logic via hooks "wrapping".

#### Example

Expand All @@ -19,24 +20,33 @@ Like `useEffect`, it performs conditions match on first mount.
## Reference

```ts
type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = (
conditions: Cond
) => boolean;

function useConditionalEffect<T extends ReadonlyArray<any>>(
callback: React.EffectCallback,
conditions: T,
deps?: React.DependencyList,
predicate?: IUseConditionalEffectPredicate<T>
export function useConditionalEffect<
Cond extends IConditionsList,
Callback extends IEffectCallback = IEffectCallback,
Deps extends DependencyList | undefined = DependencyList | undefined,
HookRestArgs extends any[] = any[],
R extends HookRestArgs = HookRestArgs
>(
callback: Callback,
deps: Deps,
conditions: Cond,
predicate: IConditionsPredicate<Cond> = truthyAndArrayPredicate,
effectHook: IEffectHook<Callback, Deps, HookRestArgs> = useEffect,
...effectHookRestArgs: R
): void;
```

#### Arguments

- **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.
- **callback** _`IEffectCallback`_ - Function that will be passed to underlying effect hook.
- **deps** _`React.DependencyList`_ - Dependencies list like for `useEffect`. If not undefined -
effect will be triggered when deps changed AND conditions are satisfying predicate.
- **conditions** _`IConditionsList`_ - Conditions array.
- **predicate** _`IUseConditionalEffectPredicate<ReadonlyArray<any>>`_ - Predicate that defines
whether conditions satisfying certain provision.
By default, it is all-truthy provision, meaning that all conditions should be truthy.
- **effectHook** _`IEffectHook<Callback, Deps, HookRestArgs>`_ - Effect hook that will be used to
run callback. Must comply `useEffect` signature, meaning that callback should be placed as first
argument and dependencies list as second.
- **...effectHookRestArgs** _`HookRestArgs`_ - Extra arguments that are passed to `effectHook`. Meaning
the arguments that are after 2nd argument.
37 changes: 31 additions & 6 deletions src/useConditionalEffect/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { truthyOrArrayPredicate, useConditionalEffect } from '../..';
import { DependencyList, EffectCallback } from 'react';
import {
truthyAndArrayPredicate,
truthyOrArrayPredicate,
useConditionalEffect,
useUpdateEffect,
} from '../..';

describe('useConditionalEffect', () => {
it('should be defined', () => {
expect(useConditionalEffect).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useConditionalEffect(() => {}, []));
const { result } = renderHook(() => useConditionalEffect(() => {}, undefined, []));
expect(result.error).toBeUndefined();
});

it('by default should invoke effect only in case all conditions are truthy', () => {
const spy = jest.fn();
const { rerender } = renderHook(({ cond }) => useConditionalEffect(spy, cond), {
const { rerender } = renderHook(({ cond }) => useConditionalEffect(spy, undefined, cond), {
initialProps: { cond: [1] as unknown[] },
});
expect(spy).toHaveBeenCalledTimes(1);
Expand All @@ -30,15 +36,15 @@ describe('useConditionalEffect', () => {

it('should not be called on mount if conditions are falsy', () => {
const spy = jest.fn();
renderHook(({ cond }) => useConditionalEffect(spy, cond), {
renderHook(({ cond }) => useConditionalEffect(spy, undefined, cond), {
initialProps: { cond: [null] as unknown[] },
});
expect(spy).toHaveBeenCalledTimes(0);
});

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), {
const { rerender } = renderHook(({ cond, deps }) => useConditionalEffect(spy, deps, cond), {
initialProps: { cond: [false] as unknown[], deps: [1] as any[] },
});
expect(spy).toHaveBeenCalledTimes(0);
Expand Down Expand Up @@ -66,7 +72,7 @@ describe('useConditionalEffect', () => {
const spy = jest.fn();
const predicateSpy = jest.fn((conditions) => truthyOrArrayPredicate(conditions));
const { rerender } = renderHook(
({ cond }) => useConditionalEffect(spy, cond, undefined, predicateSpy),
({ cond }) => useConditionalEffect(spy, undefined, cond, predicateSpy),
{
initialProps: { cond: [null] as unknown[] },
}
Expand All @@ -86,4 +92,23 @@ describe('useConditionalEffect', () => {
expect(predicateSpy).toHaveBeenCalledTimes(4);
expect(spy).toHaveBeenCalledTimes(2);
});

it('should accept custom hooks and pass extra args to it', () => {
const callbackSpy = jest.fn();
const effectSpy = jest.fn(
(cb: EffectCallback, deps: DependencyList | undefined, _num: number) =>
useUpdateEffect(cb, deps)
);
const { rerender } = renderHook(() =>
useConditionalEffect(callbackSpy, undefined, [true], truthyAndArrayPredicate, effectSpy, 123)
);

expect(callbackSpy).not.toHaveBeenCalled();
expect(effectSpy).toHaveBeenCalledTimes(1);
expect(effectSpy.mock.calls[0][2]).toBe(123);

rerender();

expect(callbackSpy).toHaveBeenCalledTimes(1);
});
});
4 changes: 2 additions & 2 deletions src/useConditionalEffect/__tests__/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ describe('useConditionalEffect', () => {
});

it('should render', () => {
const { result } = renderHook(() => useConditionalEffect(() => {}, []));
const { result } = renderHook(() => useConditionalEffect(() => {}, undefined, []));
expect(result.error).toBeUndefined();
});

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], undefined, predicateSpy));
renderHook(() => useConditionalEffect(spy, undefined, [true], predicateSpy));
expect(predicateSpy).toHaveBeenCalledTimes(0);
expect(spy).toHaveBeenCalledTimes(0);
});
Expand Down
55 changes: 36 additions & 19 deletions src/useConditionalEffect/useConditionalEffect.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import { DependencyList, EffectCallback, useEffect } from 'react';
import { truthyAndArrayPredicate } from '..';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DependencyList, useEffect } from 'react';
import { IEffectCallback, IEffectHook, truthyAndArrayPredicate } from '..';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type IUseConditionalEffectPredicate<Cond extends ReadonlyArray<any>> = (
export type IConditionsList = ReadonlyArray<any>;

export type IConditionsPredicate<Cond extends IConditionsList = IConditionsList> = (
conditions: Cond
) => boolean;

/**
* Like `useEffect` but callback invoked only if conditions match predicate.
*
* @param callback Callback to invoke
* @param conditions Conditions array
* @param deps Dependencies list like for `useEffect`. If set - effect will be
* @param callback Function that will be passed to underlying effect hook.
* @param deps Dependencies list like for `useEffect`. If not undefined - effect will be
* triggered when deps changed AND conditions are satisfying predicate.
* @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.
* @param effectHook Effect hook that will be used to run callback. Must comply `useEffect`
* signature, meaning that callback should be placed as first argument and dependencies list
* as second.
* @param effectHookRestArgs Extra arguments that are passed to `effectHook`.
*/
export function useConditionalEffect<T extends ReadonlyArray<unknown>>(
callback: EffectCallback,
conditions: T,
deps?: DependencyList,
predicate: IUseConditionalEffectPredicate<T> = truthyAndArrayPredicate
export function useConditionalEffect<
Cond extends IConditionsList,
Callback extends IEffectCallback = IEffectCallback,
Deps extends DependencyList | undefined = DependencyList | undefined,
HookRestArgs extends any[] = any[],
R extends HookRestArgs = HookRestArgs
>(
callback: Callback,
deps: Deps,
conditions: Cond,
predicate: IConditionsPredicate<Cond> = truthyAndArrayPredicate,
effectHook: IEffectHook<Callback, Deps, HookRestArgs> = useEffect,
...effectHookRestArgs: R
): void {
// eslint-disable-next-line consistent-return
useEffect(() => {
if (predicate(conditions)) {
return callback();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
effectHook(
// eslint-disable-next-line consistent-return
(() => {
if (predicate(conditions)) {
return callback();
}
}) as Callback,
deps,
...effectHookRestArgs
);
}
37 changes: 0 additions & 37 deletions src/useConditionalUpdateEffect/__docs__/example.stories.tsx

This file was deleted.

42 changes: 0 additions & 42 deletions src/useConditionalUpdateEffect/__docs__/story.mdx

This file was deleted.

Loading

0 comments on commit 4474cf7

Please sign in to comment.