From 30abe2b22e3cb7a3e4c6dedd2466d74ce660911d Mon Sep 17 00:00:00 2001 From: Anton Zinovyev Date: Sat, 23 Nov 2019 15:14:35 +0300 Subject: [PATCH] feat: add useFirstMountState & useRendersCount hooks (#769) feat(useFirstMountState): hook to track if render is first; feat(useRendersCount): hook to track renders count; refactor(useUpdateEffect): now uses useFirstMountState hook; refactor(usePreviousDistinct): now uses with useFirstMountState hook; docs: update readme; --- README.md | 2 ++ docs/useFirstMountState.md | 29 ++++++++++++++++++++ docs/useRendersCount.md | 29 ++++++++++++++++++++ src/__stories__/useFirstMountState.story.tsx | 22 +++++++++++++++ src/__stories__/useRendersCount.story.tsx | 22 +++++++++++++++ src/index.ts | 2 ++ src/useFirstMountState.ts | 13 +++++++++ src/usePreviousDistinct.ts | 7 ++--- src/useRendersCount.ts | 5 ++++ src/useUpdateEffect.ts | 11 +++----- tests/useFirstMountState.test.ts | 22 +++++++++++++++ tests/useRendersCount.test.ts | 22 +++++++++++++++ 12 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 docs/useFirstMountState.md create mode 100644 docs/useRendersCount.md create mode 100644 src/__stories__/useFirstMountState.story.tsx create mode 100644 src/__stories__/useRendersCount.story.tsx create mode 100644 src/useFirstMountState.ts create mode 100644 src/useRendersCount.ts create mode 100644 tests/useFirstMountState.test.ts create mode 100644 tests/useRendersCount.test.ts diff --git a/README.md b/README.md index 4264706cea..e35d791c00 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,8 @@ - [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo) - [`useMultiStateValidator`](./docs/useMultiStateValidator.md) — alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo) - [`useMediatedState`](./docs/useMediatedState.md) — like the regular `useState` but with mediation by custom function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemediatedstate--demo) + - [`useFirstMountState`](./docs/useFirstMountState.md) — check if current render is first. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usefirstmountstate--demo) + - [`useRendersCount`](./docs/useRendersCount.md) — count component renders. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userenderscount--demo)

- [**Miscellaneous**]() diff --git a/docs/useFirstMountState.md b/docs/useFirstMountState.md new file mode 100644 index 0000000000..8d2a737389 --- /dev/null +++ b/docs/useFirstMountState.md @@ -0,0 +1,29 @@ +# `useFirstMountState` + +Returns `true` if component is just mounted (on first render) and `false` otherwise. + +## Usage + +```typescript jsx +import * as React from 'react'; +import { useFirstMountState } from 'react-use'; + +const Demo = () => { + const isFirstMount = useFirstMountState(); + const update = useUpdate(); + + return ( +
+ This component is just mounted: {isFirstMount ? 'YES' : 'NO'} +
+ +
+ ); +}; +``` + +## Reference + +```typescript +const isFirstMount: boolean = useFirstMountState(); +``` diff --git a/docs/useRendersCount.md b/docs/useRendersCount.md new file mode 100644 index 0000000000..05f6111125 --- /dev/null +++ b/docs/useRendersCount.md @@ -0,0 +1,29 @@ +# `useRendersCount` + +Tracks compontent's renders count including the first render. + +## Usage + +```typescript jsx +import * as React from 'react'; +import { useRendersCount } from "react-use"; + +const Demo = () => { + const update = useUpdate(); + const rendersCount = useRendersCount(); + + return ( +
+ Renders count: {rendersCount} +
+ +
+ ); +}; +``` + +## Reference + +```typescript +const rendersCount: number = useRendersCount(); +``` diff --git a/src/__stories__/useFirstMountState.story.tsx b/src/__stories__/useFirstMountState.story.tsx new file mode 100644 index 0000000000..d56151d1e5 --- /dev/null +++ b/src/__stories__/useFirstMountState.story.tsx @@ -0,0 +1,22 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useFirstMountState } from '../useFirstMountState'; +import useUpdate from '../useUpdate'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const isFirstMount = useFirstMountState(); + const update = useUpdate(); + + return ( +
+ This component is just mounted: {isFirstMount ? 'YES' : 'NO'} +
+ +
+ ); +}; + +storiesOf('State|useFirstMountState', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/__stories__/useRendersCount.story.tsx b/src/__stories__/useRendersCount.story.tsx new file mode 100644 index 0000000000..2b07ac377a --- /dev/null +++ b/src/__stories__/useRendersCount.story.tsx @@ -0,0 +1,22 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useRendersCount } from '../useRendersCount'; +import useUpdate from '../useUpdate'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const update = useUpdate(); + const rendersCount = useRendersCount(); + + return ( +
+ Renders count: {rendersCount} +
+ +
+ ); +}; + +storiesOf('State|useRendersCount', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/index.ts b/src/index.ts index 1b4dd8946e..08e4383a34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,4 +94,6 @@ export { useMultiStateValidator } from './useMultiStateValidator'; export { default as useWindowScroll } from './useWindowScroll'; export { default as useWindowSize } from './useWindowSize'; export { default as useMeasure } from './useMeasure'; +export { useRendersCount } from './useRendersCount'; +export { useFirstMountState } from './useFirstMountState'; export { default as useSet } from './useSet'; diff --git a/src/useFirstMountState.ts b/src/useFirstMountState.ts new file mode 100644 index 0000000000..cf210622a2 --- /dev/null +++ b/src/useFirstMountState.ts @@ -0,0 +1,13 @@ +import { useRef } from 'react'; + +export function useFirstMountState(): boolean { + const isFirst = useRef(true); + + if (isFirst.current) { + isFirst.current = false; + + return true; + } + + return isFirst.current; +} diff --git a/src/usePreviousDistinct.ts b/src/usePreviousDistinct.ts index 9e44524ac1..896afc73fe 100644 --- a/src/usePreviousDistinct.ts +++ b/src/usePreviousDistinct.ts @@ -1,4 +1,5 @@ import { useRef } from 'react'; +import { useFirstMountState } from './useFirstMountState'; export type Predicate = (prev: T | undefined, next: T) => boolean; @@ -7,11 +8,9 @@ const strictEquals = (prev: T | undefined, next: T) => prev === next; export default function usePreviousDistinct(value: T, compare: Predicate = strictEquals): T | undefined { const prevRef = useRef(); const curRef = useRef(value); - const firstRender = useRef(true); + const isFirstMount = useFirstMountState(); - if (firstRender.current) { - firstRender.current = false; - } else if (!compare(curRef.current, value)) { + if (!isFirstMount && !compare(curRef.current, value)) { prevRef.current = curRef.current; curRef.current = value; } diff --git a/src/useRendersCount.ts b/src/useRendersCount.ts new file mode 100644 index 0000000000..ccb8681b8b --- /dev/null +++ b/src/useRendersCount.ts @@ -0,0 +1,5 @@ +import { useRef } from 'react'; + +export function useRendersCount(): number { + return ++useRef(0).current; +} diff --git a/src/useUpdateEffect.ts b/src/useUpdateEffect.ts index 63fbc80c66..c180d73700 100644 --- a/src/useUpdateEffect.ts +++ b/src/useUpdateEffect.ts @@ -1,14 +1,11 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; +import { useFirstMountState } from './useFirstMountState'; const useUpdateEffect: typeof useEffect = (effect, deps) => { - const isInitialMount = useRef(true); + const isFirstMount = useFirstMountState(); useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - } else { - return effect(); - } + !isFirstMount && effect(); }, deps); }; diff --git a/tests/useFirstMountState.test.ts b/tests/useFirstMountState.test.ts new file mode 100644 index 0000000000..c6a677a973 --- /dev/null +++ b/tests/useFirstMountState.test.ts @@ -0,0 +1,22 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useFirstMountState } from '../src'; + +describe('useFirstMountState', () => { + it('should be defined', () => { + expect(useFirstMountState).toBeDefined(); + }); + + it('should return boolean', () => { + expect(renderHook(() => useFirstMountState()).result.current).toEqual(expect.any(Boolean)); + }); + + it('should return true on first render and false on all others', () => { + const hook = renderHook(() => useFirstMountState()); + + expect(hook.result.current).toBe(true); + hook.rerender(); + expect(hook.result.current).toBe(false); + hook.rerender(); + expect(hook.result.current).toBe(false); + }); +}); diff --git a/tests/useRendersCount.test.ts b/tests/useRendersCount.test.ts new file mode 100644 index 0000000000..a18122a0c5 --- /dev/null +++ b/tests/useRendersCount.test.ts @@ -0,0 +1,22 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useRendersCount } from '../src'; + +describe('useRendersCount', () => { + it('should be defined', () => { + expect(useRendersCount).toBeDefined(); + }); + + it('should return number', () => { + expect(renderHook(() => useRendersCount()).result.current).toEqual(expect.any(Number)); + }); + + it('should return actual number of renders', () => { + const hook = renderHook(() => useRendersCount()); + + expect(hook.result.current).toBe(1); + hook.rerender(); + expect(hook.result.current).toBe(2); + hook.rerender(); + expect(hook.result.current).toBe(3); + }); +});