From cffdd6d3a142e6201fd1fed92010678e4f924fe7 Mon Sep 17 00:00:00 2001 From: hyesung oh Date: Mon, 31 Oct 2022 23:27:24 +0900 Subject: [PATCH] =?UTF-8?q?Life=20cycle=20hook=20=EA=B0=9C=EB=B0=9C=20=20(?= =?UTF-8?q?#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 6 +- package.json | 1 + src/hooks/life-cycle/useDidMount.test.tsx | 60 +++++++++++++++++++ src/hooks/life-cycle/useDidMount.ts | 13 ++++ src/hooks/life-cycle/useDidUpdate.test.tsx | 54 +++++++++++++++++ src/hooks/life-cycle/useDidUpdate.ts | 16 +++++ src/hooks/life-cycle/useWillUnmount.test.tsx | 63 ++++++++++++++++++++ src/hooks/life-cycle/useWillUnmount.ts | 9 +++ 8 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/hooks/life-cycle/useDidMount.test.tsx create mode 100644 src/hooks/life-cycle/useDidMount.ts create mode 100644 src/hooks/life-cycle/useDidUpdate.test.tsx create mode 100644 src/hooks/life-cycle/useDidUpdate.ts create mode 100644 src/hooks/life-cycle/useWillUnmount.test.tsx create mode 100644 src/hooks/life-cycle/useWillUnmount.ts diff --git a/.eslintrc.js b/.eslintrc.js index a3c6f8f3..630551ce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -70,6 +70,7 @@ module.exports = { 'import/no-unresolved': 'error', 'react/react-in-jsx-scope': 'off', 'no-use-before-define': 'off', + 'no-restricted-exports': 'off', '@typescript-eslint/no-use-before-define': 'off', 'simple-import-sort/imports': [ 'error', @@ -94,7 +95,10 @@ module.exports = { 'import/first': 'error', 'import/newline-after-import': 'error', 'import/no-duplicates': 'error', - '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { ignoreRestSiblings: true, argsIgnorePattern: '_', varsIgnorePattern: '_' }, + ], 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ 'error', diff --git a/package.json b/package.json index f576b3b6..5af254b6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "next start", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", "test:unit": "jest", + "test:watch": "jest --watch", "test:coverage": "yarn test:unit --coverage", "cypress:open": "cypress open", "cypress:run": "cypress run", diff --git a/src/hooks/life-cycle/useDidMount.test.tsx b/src/hooks/life-cycle/useDidMount.test.tsx new file mode 100644 index 00000000..786bccb8 --- /dev/null +++ b/src/hooks/life-cycle/useDidMount.test.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; + +import useDidMount from './useDidMount'; + +describe('useDidMount', () => { + it('default export이여야 한다', () => { + expect(useDidMount).toBeDefined(); + }); + + it('effectCallback이 실행되어야 한다', () => { + const effectCallback = jest.fn(); + renderHook(() => useDidMount(effectCallback)); + expect(effectCallback).toBeCalled(); + }); + + it('rerender 시 1번 실행되어야 한다', () => { + const effectCallback = jest.fn(); + const { rerender } = renderHook(() => useDidMount(effectCallback)); + rerender(); + expect(effectCallback).toBeCalledTimes(1); + }); + + describe('useDidMount Component', () => { + const STATE_CHANGE_BUTTON_TEXT = 'change'; + const mockCallback = jest.fn(); + + const App = () => { + const [_, setState] = useState(false); + + useDidMount(mockCallback); + + return ( +
+ +
+ ); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('mockCallback이 실행되어야 한다', () => { + render(); + expect(mockCallback).toBeCalledTimes(1); + }); + + it('mockCallback은 상태가 변해도 1번 실행되어야 한다', () => { + render(); + expect(mockCallback).toBeCalledTimes(1); + const setStateButton = screen.getByText(STATE_CHANGE_BUTTON_TEXT); + fireEvent.click(setStateButton); + expect(mockCallback).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/hooks/life-cycle/useDidMount.ts b/src/hooks/life-cycle/useDidMount.ts new file mode 100644 index 00000000..34a5b3b7 --- /dev/null +++ b/src/hooks/life-cycle/useDidMount.ts @@ -0,0 +1,13 @@ +import { useEffect, useRef } from 'react'; + +const useDidMount = (callback: VoidFunction) => { + const didMountRef = useRef(false); + + useEffect(() => { + if (didMountRef.current) return; + didMountRef.current = true; + callback(); + }, []); +}; + +export default useDidMount; diff --git a/src/hooks/life-cycle/useDidUpdate.test.tsx b/src/hooks/life-cycle/useDidUpdate.test.tsx new file mode 100644 index 00000000..05afb974 --- /dev/null +++ b/src/hooks/life-cycle/useDidUpdate.test.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; + +import useDidUpdate from './useDidUpdate'; + +describe('useDidUpdate', () => { + it('default export이여야 한다', () => { + expect(useDidUpdate).toBeDefined(); + }); + + it('첫 호출 시 effectCallback이 실행되면 안된다', () => { + const mockCallback = jest.fn(); + renderHook(() => useDidUpdate(mockCallback, [])); + expect(mockCallback).not.toBeCalled(); + }); + + describe('useDidUpdate Component', () => { + const STATE_CHANGE_BUTTON_TEXT = 'change'; + const mockCallback = jest.fn(); + + const App = () => { + const [state, setState] = useState(0); + + useDidUpdate(mockCallback, [state]); + + return ( +
+ +
+ ); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('마운트 시 effectCallback이 실행되면 안된다', () => { + render(); + expect(mockCallback).not.toBeCalled(); + }); + + it('dependency list 업데이트 시 effectCallback이 실행되어야 한다', () => { + render(); + const setStateButton = screen.getByText(STATE_CHANGE_BUTTON_TEXT); + fireEvent.click(setStateButton); + expect(mockCallback).toBeCalledTimes(1); + fireEvent.click(setStateButton); + expect(mockCallback).toBeCalledTimes(2); + }); + }); +}); diff --git a/src/hooks/life-cycle/useDidUpdate.ts b/src/hooks/life-cycle/useDidUpdate.ts new file mode 100644 index 00000000..1c7eee69 --- /dev/null +++ b/src/hooks/life-cycle/useDidUpdate.ts @@ -0,0 +1,16 @@ +import { DependencyList, useEffect, useRef } from 'react'; + +const useDidUpdate = (callback: VoidFunction, dependencyList: DependencyList) => { + const didMountRef = useRef(false); + + useEffect(() => { + if (!didMountRef.current) { + didMountRef.current = true; + return; + } + + callback(); + }, [...dependencyList]); +}; + +export default useDidUpdate; diff --git a/src/hooks/life-cycle/useWillUnmount.test.tsx b/src/hooks/life-cycle/useWillUnmount.test.tsx new file mode 100644 index 00000000..50f53bc2 --- /dev/null +++ b/src/hooks/life-cycle/useWillUnmount.test.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; + +import useWillUnmount from './useWillUnmount'; + +describe('useWillUnmount', () => { + it('default export이어야 한다', () => { + expect(useWillUnmount).toBeDefined(); + }); + + it('첫 호출 시 callback이 실행되면 안된다', () => { + const mockCallback = jest.fn(); + renderHook(() => useWillUnmount(mockCallback)); + expect(mockCallback).not.toBeCalled(); + }); + + describe('useWillUnmount Component', () => { + const TOGGLE_BUTTON_TEXT = 'toggle'; + const mockCallback = jest.fn(); + + const Child = () => { + useWillUnmount(mockCallback); + + return
; + }; + + const App = () => { + const [state, setState] = useState(true); + + return ( +
+ + + {state && } +
+ ); + }; + + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + + it('마운트 시 callback이 실행되면 안된다', () => { + render(); + expect(mockCallback).not.toBeCalled(); + }); + + it('Child가 unmount시 callback이 실행된다', () => { + render(); + const toggleButton = screen.getByText(TOGGLE_BUTTON_TEXT); + fireEvent.click(toggleButton); // false + expect(mockCallback).toBeCalledTimes(1); + + fireEvent.click(toggleButton); // true + fireEvent.click(toggleButton); // false + expect(mockCallback).toBeCalledTimes(2); + }); + }); +}); diff --git a/src/hooks/life-cycle/useWillUnmount.ts b/src/hooks/life-cycle/useWillUnmount.ts new file mode 100644 index 00000000..7daf702c --- /dev/null +++ b/src/hooks/life-cycle/useWillUnmount.ts @@ -0,0 +1,9 @@ +import { useEffect } from 'react'; + +const useWillUnmount = (callback: VoidFunction) => { + useEffect(() => { + return callback; + }, []); +}; + +export default useWillUnmount;