From bd56af3c775123450867931aa07a33eaf08415e7 Mon Sep 17 00:00:00 2001 From: Anton Zinovyev Date: Fri, 16 Jul 2021 16:19:59 +0300 Subject: [PATCH] feat: deps for `useConditionalUpdateEffect` and `useConditionalEffect` (#201) Add new argument for above functions. fix: 157 BREAKING CHANGE: `useConditionalUpdateEffect` and `useConditionalEffect` now has changed call signature (new argument). --- src/index.ts | 2 + .../__docs__/example.stories.tsx | 1 + src/useConditionalEffect/__docs__/story.mdx | 10 +++-- src/useConditionalEffect/__tests__/dom.ts | 39 ++++++++++++++++--- src/useConditionalEffect/__tests__/ssr.ts | 6 +-- .../useConditionalEffect.ts | 31 +++++++-------- .../__docs__/example.stories.tsx | 1 + .../__docs__/story.mdx | 6 ++- .../__tests__/dom.ts | 32 +++++++++++++-- .../__tests__/ssr.ts | 2 +- .../useConditionalUpdateEffect.ts | 32 +++++++-------- src/util/const.ts | 7 +++- 12 files changed, 115 insertions(+), 54 deletions(-) diff --git a/src/index.ts b/src/index.ts index c6f4a981..abdbb8da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/useConditionalEffect/__docs__/example.stories.tsx b/src/useConditionalEffect/__docs__/example.stories.tsx index 46e4dec4..80e50cb8 100644 --- a/src/useConditionalEffect/__docs__/example.stories.tsx +++ b/src/useConditionalEffect/__docs__/example.stories.tsx @@ -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) ); diff --git a/src/useConditionalEffect/__docs__/story.mdx b/src/useConditionalEffect/__docs__/story.mdx index a9cb611e..a1751ca3 100644 --- a/src/useConditionalEffect/__docs__/story.mdx +++ b/src/useConditionalEffect/__docs__/story.mdx @@ -26,13 +26,17 @@ type IUseConditionalEffectPredicate> = ( function useConditionalEffect>( callback: React.EffectCallback, conditions: T, + deps?: React.DependencyList, predicate?: IUseConditionalEffectPredicate ): void; ``` #### Arguments -- _**callback**_ _`React.EffectCallback`_ - Effect callback like for `React.useEffect` hook -- _**conditions**_ _`ReadonlyArray`_ - Conditions that are matched against predicate -- _**predicate**_ _`IUseConditionalEffectPredicate>`_ - Predicate that matches conditions. +- **callback** _`React.EffectCallback`_ - Effect callback like for `React.useEffect` hook +- **conditions** _`ReadonlyArray`_ - 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>`_ - Predicate that matches + conditions. By default, it is all-truthy provision, meaning that all conditions should be truthy. diff --git a/src/useConditionalEffect/__tests__/dom.ts b/src/useConditionalEffect/__tests__/dom.ts index 1ae4ded6..7e58f747 100644 --- a/src/useConditionalEffect/__tests__/dom.ts +++ b/src/useConditionalEffect/__tests__/dom.ts @@ -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', () => { @@ -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); diff --git a/src/useConditionalEffect/__tests__/ssr.ts b/src/useConditionalEffect/__tests__/ssr.ts index 3d3110a5..586ce326 100644 --- a/src/useConditionalEffect/__tests__/ssr.ts +++ b/src/useConditionalEffect/__tests__/ssr.ts @@ -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); }); }); diff --git a/src/useConditionalEffect/useConditionalEffect.ts b/src/useConditionalEffect/useConditionalEffect.ts index d8d70185..895c01d0 100644 --- a/src/useConditionalEffect/useConditionalEffect.ts +++ b/src/useConditionalEffect/useConditionalEffect.ts @@ -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> = ( @@ -11,26 +11,23 @@ export type IUseConditionalEffectPredicate> = ( * * @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>( +export function useConditionalEffect>( callback: EffectCallback, conditions: T, - predicate: IUseConditionalEffectPredicate = truthyArrayItemsPredicate + deps?: DependencyList, + predicate: IUseConditionalEffectPredicate = 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); } diff --git a/src/useConditionalUpdateEffect/__docs__/example.stories.tsx b/src/useConditionalUpdateEffect/__docs__/example.stories.tsx index d8048956..b6c73b0e 100644 --- a/src/useConditionalUpdateEffect/__docs__/example.stories.tsx +++ b/src/useConditionalUpdateEffect/__docs__/example.stories.tsx @@ -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) ); diff --git a/src/useConditionalUpdateEffect/__docs__/story.mdx b/src/useConditionalUpdateEffect/__docs__/story.mdx index 6fc6dded..a8c2599f 100644 --- a/src/useConditionalUpdateEffect/__docs__/story.mdx +++ b/src/useConditionalUpdateEffect/__docs__/story.mdx @@ -26,6 +26,7 @@ type IUseConditionalUpdateEffectPredicate> = ( function useConditionalUpdateEffect>( callback: React.EffectCallback, conditions: T, + deps?: React.DependencyList, predicate?: IUseConditionalUpdateEffectPredicate ): void; ``` @@ -34,5 +35,8 @@ function useConditionalUpdateEffect>( - _**callback**_ _`React.EffectCallback`_ - Effect callback like for `React.useEffect` hook - _**conditions**_ _`ReadonlyArray`_ - Conditions that are matched against predicate -- _**predicate**_ _`IUseConditionalUpdateEffectPredicate>`_ - 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>`_ - Predicate that + matches conditions. By default, it is all-truthy provision, meaning that all conditions should be truthy. diff --git a/src/useConditionalUpdateEffect/__tests__/dom.ts b/src/useConditionalUpdateEffect/__tests__/dom.ts index 2d3b9dae..748b5d8a 100644 --- a/src/useConditionalUpdateEffect/__tests__/dom.ts +++ b/src/useConditionalUpdateEffect/__tests__/dom.ts @@ -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', () => { @@ -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); }); @@ -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[] }, } diff --git a/src/useConditionalUpdateEffect/__tests__/ssr.ts b/src/useConditionalUpdateEffect/__tests__/ssr.ts index 3998628c..15f7fa06 100644 --- a/src/useConditionalUpdateEffect/__tests__/ssr.ts +++ b/src/useConditionalUpdateEffect/__tests__/ssr.ts @@ -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); }); diff --git a/src/useConditionalUpdateEffect/useConditionalUpdateEffect.ts b/src/useConditionalUpdateEffect/useConditionalUpdateEffect.ts index 220a0786..828de73a 100644 --- a/src/useConditionalUpdateEffect/useConditionalUpdateEffect.ts +++ b/src/useConditionalUpdateEffect/useConditionalUpdateEffect.ts @@ -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> = ( @@ -12,27 +11,22 @@ export type IUseConditionalUpdateEffectPredicate * * @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>( +export function useConditionalUpdateEffect>( callback: EffectCallback, conditions: T, - predicate: IUseConditionalUpdateEffectPredicate = truthyArrayItemsPredicate + deps?: DependencyList, + predicate: IUseConditionalUpdateEffectPredicate = 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); } diff --git a/src/util/const.ts b/src/util/const.ts index 7af5ce00..f1f14580 100644 --- a/src/util/const.ts +++ b/src/util/const.ts @@ -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): boolean { +export function truthyAndArrayPredicate(conditions: ReadonlyArray): boolean { return conditions.every((i) => Boolean(i)); } + +export function truthyOrArrayPredicate(conditions: ReadonlyArray): boolean { + return conditions.some((i) => Boolean(i)); +}