Repository: streamich/react-use
Pull request #769 (2019-11-13 13:13:26)

feat: add useFirstMountState & useRendersCount hooks
feat(useFirstMountState): hook to track if render is first;
feat(useRendersCount): hook to track renders count;
feat(useUpdateEffect): refactored with useFirstMountState hook;
feat(usePreviousDistinct): refactored with useFirstMountState hook;

# Description

<!-- Please include a summary of the change along with relevant motivation and context. -->


## Type of change

<!-- Check all relevant options. -->
- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [x] New feature _(non-breaking change which adds functionality)_
- [ ] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_

# Checklist
- [x] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md)
- [x] Perform a code self-review
- [x] Comment the code, particularly in hard-to-understand areas
- [x] Add documentation
- [x] Add hook's story at Storybook
- [x] Cover changes with tests
- [x] Ensure the test suite passes (`yarn test`)
- [x] Provide 100% tests coverage
- [x] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure.
- [x] Make sure types are fine (`yarn lint:types`).

<!-- If you can't check all the checkboxes right now - check what you can, create a Draft PR, make some changes if needed and get back to it when you will be able to put some marks in list. -->



Patch:
diff --git a/README.md b/README.md
index 4264706c..e35d791c 100644
--- a/README.md
+++ b/README.md
@@ -139,6 +139,8 @@
   - [`useStateValidator`](./docs/useStateValidator.md) &mdash; tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo)
   - [`useMultiStateValidator`](./docs/useMultiStateValidator.md) &mdash; 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) &mdash; 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) &mdash; check if current render is first. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usefirstmountstate--demo)
+  - [`useRendersCount`](./docs/useRendersCount.md) &mdash; count component renders. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userenderscount--demo)
     <br/>
     <br/>
 - [**Miscellaneous**]()
diff --git a/docs/useFirstMountState.md b/docs/useFirstMountState.md
new file mode 100644
index 00000000..8d2a7373
--- /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 (
+    <div>
+      <span>This component is just mounted: {isFirstMount ? 'YES' : 'NO'}</span>
+      <br />
+      <button onClick={update}>re-render</button>
+    </div>
+  );
+};
+```
+
+## Reference
+
+```typescript
+const isFirstMount: boolean = useFirstMountState();
+```
diff --git a/docs/useRendersCount.md b/docs/useRendersCount.md
new file mode 100644
index 00000000..05f61111
--- /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 (
+    <div>
+      <span>Renders count: {rendersCount}</span>
+      <br />
+      <button onClick={update}>re-render</button>
+    </div>
+  );
+};
+```
+
+## 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 00000000..d56151d1
--- /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 (
+    <div>
+      <span>This component is just mounted: {isFirstMount ? 'YES' : 'NO'}</span>
+      <br />
+      <button onClick={update}>re-render</button>
+    </div>
+  );
+};
+
+storiesOf('State|useFirstMountState', module)
+  .add('Docs', () => <ShowDocs md={require('../../docs/useFirstMountState.md')} />)
+  .add('Demo', () => <Demo />);
diff --git a/src/__stories__/useRendersCount.story.tsx b/src/__stories__/useRendersCount.story.tsx
new file mode 100644
index 00000000..2b07ac37
--- /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 (
+    <div>
+      <span>Renders count: {rendersCount}</span>
+      <br />
+      <button onClick={update}>re-render</button>
+    </div>
+  );
+};
+
+storiesOf('State|useRendersCount', module)
+  .add('Docs', () => <ShowDocs md={require('../../docs/useRendersCount.md')} />)
+  .add('Demo', () => <Demo />);
diff --git a/src/index.ts b/src/index.ts
index 1b4dd894..08e4383a 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 00000000..cf210622
--- /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 9e44524a..896afc73 100644
--- a/src/usePreviousDistinct.ts
+++ b/src/usePreviousDistinct.ts
@@ -1,4 +1,5 @@
 import { useRef } from 'react';
+import { useFirstMountState } from './useFirstMountState';
 
 export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
 
@@ -7,11 +8,9 @@ const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
 export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
   const prevRef = useRef<T>();
   const curRef = useRef<T>(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 00000000..ccb8681b
--- /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 63fbc80c..c180d737 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);
 };
 


Test patch:
diff --git a/tests/useFirstMountState.test.ts b/tests/useFirstMountState.test.ts
new file mode 100644
index 00000000..c6a677a9
--- /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 00000000..a18122a0
--- /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);
+  });
+});