├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── README.md ├── __tests__ └── modules.test.mjs ├── commitlint.config.js ├── esm.mjs ├── package.json ├── src ├── index.ts ├── useIsomorphicLayoutEffect.native.ts └── useIsomorphicLayoutEffect.ts ├── tsconfig.json └── yarn.lock /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | .yarn/install-state.gz 19 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} 20 | restore-keys: | 21 | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 22 | ${{ runner.os }}-yarn- 23 | 24 | - name: Install dependencies 25 | if: steps.yarn-cache.outputs.cache-hit != 'true' 26 | run: yarn install --frozen-lockfile 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Build package 36 | run: yarn prepare 37 | 38 | - name: Test package 39 | run: yarn test 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release package 2 | on: 3 | workflow_run: 4 | branches: 5 | - main 6 | workflows: 7 | - CI 8 | types: 9 | - completed 10 | 11 | jobs: 12 | check-commit: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | outputs: 16 | skip: ${{ steps.commit-message.outputs.skip }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Get commit message 22 | id: commit-message 23 | run: | 24 | MESSAGE=$(git log --format=%B -n 1 $(git log -1 --pretty=format:"%h")) 25 | 26 | if [[ $MESSAGE == "chore: release "* ]]; then 27 | echo "skip=true" >> $GITHUB_OUTPUT 28 | fi 29 | 30 | release: 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: read 34 | id-token: write 35 | needs: check-commit 36 | if: ${{ needs.check-commit.outputs.skip != 'true' }} 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v3 40 | with: 41 | fetch-depth: 0 42 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Configure Git 48 | run: | 49 | git config user.name "${GITHUB_ACTOR}" 50 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 51 | 52 | - name: Create release 53 | run: | 54 | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 55 | yarn release-it --ci 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 59 | NPM_CONFIG_PROVENANCE: true 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .history 3 | 4 | node_modules 5 | lib 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### [0.2.3](https://github.com/satya164/use-latest-callback/compare/v0.2.2...v0.2.3) (2024-11-14) 4 | 5 | ### [0.2.2](https://github.com/satya164/use-latest-callback/compare/v0.2.1...v0.2.2) (2024-11-14) 6 | 7 | ### [0.2.1](https://github.com/satya164/use-latest-callback/compare/v0.2.0...v0.2.1) (2024-07-10) 8 | 9 | ## [0.2.0](https://github.com/satya164/use-latest-callback/compare/v0.1.11...v0.2.0) (2024-07-10) 10 | 11 | 12 | ### Features 13 | 14 | * remove .default from commonjs output ([80c3cb2](https://github.com/satya164/use-latest-callback/commit/80c3cb2e01b3d6d63bae052f2376493baae6656e)) 15 | 16 | ### [0.1.11](https://github.com/satya164/use-latest-callback/compare/v0.1.10...v0.1.11) (2024-07-07) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * fix missing file in files field ([2857de3](https://github.com/satya164/use-latest-callback/commit/2857de3d30a1598b915cb948f8d0138f4abc7010)) 22 | 23 | ### [0.1.10](https://github.com/satya164/use-latest-callback/compare/v0.1.9...v0.1.10) (2024-07-07) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * make the exports esm compatible ([626d3fd](https://github.com/satya164/use-latest-callback/commit/626d3fdfbb1c262e5d908248f8a463f37b689b96)) 29 | 30 | ### [0.1.9](https://github.com/satya164/use-latest-callback/compare/v0.1.8...v0.1.9) (2023-11-09) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * fix paths in `package.json` ([13828a2](https://github.com/satya164/use-latest-callback/commit/13828a21077f8885a2b00ab0a15badc3a4e3a3c6)) 36 | 37 | ### [0.1.8](https://github.com/satya164/use-latest-callback/compare/v0.1.7...v0.1.8) (2023-11-09) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-latest-callback 2 | 3 | React hook which returns the latest callback without changing the reference. 4 | 5 | This is useful for scenarios such as event listeners where you may not want to resubscribe when the callback changes. 6 | 7 | ## Installation 8 | 9 | Open a Terminal in the project root and run: 10 | 11 | ```sh 12 | npm install use-latest-callback 13 | ``` 14 | 15 | ## Usage 16 | 17 | The `useLatestCallback` hook accepts a function as its argument and returns a function that preserves its reference across renders. 18 | 19 | ```js 20 | import useLatestCallback from 'use-latest-callback'; 21 | 22 | // ... 23 | 24 | function MyComponent() { 25 | const callback = useLatestCallback((value) => { 26 | console.log('Changed', value); 27 | }); 28 | 29 | React.useEffect(() => { 30 | someEvent.addListener(callback); 31 | 32 | return () => someEvent.removeListener(callback); 33 | }, [callback]); 34 | 35 | return <>{/* whatever */}; 36 | } 37 | ``` 38 | 39 | It's important to note that the callback is not intended to be called during the render phase. Only call the callback in response to an event. 40 | -------------------------------------------------------------------------------- /__tests__/modules.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import test from 'node:test'; 3 | import { createRequire } from 'node:module'; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | test('import', async () => { 8 | // eslint-disable-next-line import/no-unresolved 9 | const result = await import('use-latest-callback'); 10 | 11 | assert.strictEqual(typeof result.default, 'function'); 12 | assert.strictEqual(result.default.name, 'useLatestCallback'); 13 | }); 14 | 15 | test('require', () => { 16 | const result = require('..'); 17 | 18 | assert.strictEqual(typeof result, 'function'); 19 | assert.strictEqual(result.name, 'useLatestCallback'); 20 | }); 21 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /esm.mjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | import useLatestCallback from './lib/src/index.js'; 3 | 4 | export default useLatestCallback; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-latest-callback", 3 | "version": "0.2.3", 4 | "description": "React hook which returns the latest callback without changing the reference", 5 | "repository": "https://github.com/satya164/use-latest-callback", 6 | "author": "Satyajit Sahoo ", 7 | "license": "MIT", 8 | "keywords": [ 9 | "react", 10 | "use-event", 11 | "use-callback" 12 | ], 13 | "publishConfig": { 14 | "registry": "https://registry.npmjs.org/" 15 | }, 16 | "type": "commonjs", 17 | "source": "./src/index.ts", 18 | "main": "./lib/src/index.js", 19 | "types": "./lib/src/index.d.ts", 20 | "exports": { 21 | ".": { 22 | "types": "./lib/src/index.d.ts", 23 | "import": "./esm.mjs", 24 | "require": "./lib/src/index.js" 25 | } 26 | }, 27 | "files": [ 28 | "src", 29 | "lib", 30 | "esm.mjs" 31 | ], 32 | "scripts": { 33 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 34 | "test": "node --test", 35 | "typecheck": "tsc --noEmit", 36 | "prebuild": "del lib", 37 | "build": "tsc --declaration", 38 | "prepare": "yarn build", 39 | "release": "release-it" 40 | }, 41 | "peerDependencies": { 42 | "react": ">=16.8" 43 | }, 44 | "devDependencies": { 45 | "@commitlint/config-conventional": "^12.1.4", 46 | "@release-it/conventional-changelog": "^7.0.2", 47 | "@types/react": "^18.0.10", 48 | "commitlint": "^12.1.4", 49 | "del-cli": "^4.0.1", 50 | "eslint": "^8.53.0", 51 | "eslint-config-satya164": "^3.2.0", 52 | "prettier": "^3.0.3", 53 | "react": "^17.0.2", 54 | "release-it": "^16.2.1", 55 | "typescript": "^5.2.2" 56 | }, 57 | "eslintConfig": { 58 | "extends": "satya164", 59 | "env": { 60 | "node": true, 61 | "browser": true 62 | }, 63 | "rules": { 64 | "import/no-commonjs": "off" 65 | } 66 | }, 67 | "eslintIgnore": [ 68 | "node_modules/", 69 | "lib/" 70 | ], 71 | "release-it": { 72 | "git": { 73 | "commitMessage": "chore: release ${version}", 74 | "tagName": "v${version}" 75 | }, 76 | "npm": { 77 | "publish": true 78 | }, 79 | "github": { 80 | "release": true 81 | }, 82 | "plugins": { 83 | "@release-it/conventional-changelog": { 84 | "preset": { 85 | "name": "conventionalcommits" 86 | }, 87 | "infile": "CHANGELOG.md" 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; 3 | 4 | /** 5 | * React hook which returns the latest callback without changing the reference. 6 | */ 7 | // eslint-disable-next-line @typescript-eslint/ban-types 8 | function useLatestCallback(callback: T): T { 9 | const ref = React.useRef(callback); 10 | 11 | const latestCallback = React.useRef(function latestCallback( 12 | this: unknown, 13 | ...args: unknown[] 14 | ) { 15 | return ref.current.apply(this, args); 16 | } as unknown as T).current; 17 | 18 | useIsomorphicLayoutEffect(() => { 19 | ref.current = callback; 20 | }); 21 | 22 | return latestCallback; 23 | } 24 | 25 | // Use export assignment to compile to module.exports = 26 | export = useLatestCallback; 27 | -------------------------------------------------------------------------------- /src/useIsomorphicLayoutEffect.native.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | 3 | export default useLayoutEffect; 4 | -------------------------------------------------------------------------------- /src/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | 3 | /** 4 | * Use `useEffect` during SSR and `useLayoutEffect` in the browser to avoid warnings. 5 | */ 6 | const useIsomorphicLayoutEffect = 7 | typeof document !== 'undefined' ? useLayoutEffect : useEffect; 8 | 9 | export default useIsomorphicLayoutEffect; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "lib", 5 | "target": "ES5", 6 | "module": "CommonJS", 7 | "declaration": true, 8 | "allowUnreachableCode": false, 9 | "allowUnusedLabels": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["ESNext", "DOM"], 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noImplicitUseStrict": false, 16 | "noStrictGenericChecks": false, 17 | "noUncheckedIndexedAccess": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "strict": true 23 | } 24 | } 25 | --------------------------------------------------------------------------------