├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── demo ├── RendersCount.tsx ├── index.html ├── index.tsx └── style.css ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | dist 83 | 84 | demoBuild -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .cache 3 | demo 4 | 5 | #tests 6 | test 7 | coverage 8 | 9 | #build tools 10 | .travis.yml 11 | .jenkins.yml 12 | .codeclimate.yml 13 | 14 | #linters 15 | .jscsrc 16 | .jshintrc 17 | .eslintrc* 18 | 19 | #editor settings 20 | .idea 21 | .editorconfig 22 | 23 | yarn.lock 24 | tsconfig.json 25 | .prettierrc 26 | .gitignore 27 | demoBuild -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always" 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Adam Pietrasiak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Useage 2 | 3 | `yarn add use-method` 4 | 5 | ```ts 6 | import { useMethod } from 'use-method'; 7 | 8 | function MyComponent() { 9 | const randomNumber = Math.random(); 10 | 11 | // returns function that keeps the same reference during entire lifecycle of the component, while always using 'fresh' variables from last render 12 | const hiMethod = useMethod((name) => { 13 | // it'll always have `randomNumber` variable from the last render 14 | alert(`Hi, ${name} (${randomNumber})!`); 15 | }); 16 | 17 | // hiMethod reference will remain the same on every render - if you'll pass it to other components like it'll never re-render if it's memo or PureComponent 18 | } 19 | ``` 20 | 21 | # Introduction & Rationale 22 | 23 | When hooks got introduced - it's became problematic to keep functions references the same on each render. 24 | 25 | With classes it's not a problem. You just call `this.handleClick` which has the same reference on every render. 26 | 27 | With functional components, you define such functions during rendering like 28 | 29 | ```tsx 30 | function SomeComponent() { 31 | function handleClick() { 32 | // handling click 33 | } 34 | 35 | // rendering 36 | } 37 | ``` 38 | 39 | Without any optimization - `handleClick` would result in a brand new function on every render. 40 | 41 | React `useCallback` hook is created to help solving this problem. 42 | 43 | But even `useCallback` updates function reference when variables used as dependencies change. 44 | 45 | Also - useCallback dependencies list tend to 'pollute' the code and might make it easy to introduce nasty bugs if you don't use proper linter. 46 | 47 | `use-method` makes provided function behave like class method. It will have the same reference on every render, but it'll use values from last render. 48 | 49 | Let's consider such example: 50 | 51 | ```tsx 52 | function PeopleToggler({ people, onToggle }) { 53 | const togglePerson = useMethod((person) => { 54 | // we want to always use `fresh` people and onToggle props here 55 | const peopleAfterToggle = oggleInArray(people, person); 56 | onToggle(peopleAfterToggle); 57 | }); 58 | 59 | return ; 60 | } 61 | ``` 62 | 63 | When `togglePerson` is called - it has access to fresh `people` and `onToggle` values. 64 | 65 | At the same time, `togglePerson` reference remains constant. 66 | 67 | The same component using `useCallback` could look like: 68 | 69 | ```tsx 70 | function PeopleToggler({ people, onToggle }) { 71 | const togglePerson = useCallback( 72 | (person) => { 73 | // we need fresh `people` and `onToggle` here 74 | const peopleAfterToggle = oggleInArray(people, person); 75 | onToggle(peopleAfterToggle); 76 | }, 77 | [people, onToggle], 78 | ); 79 | 80 | return ; 81 | } 82 | ``` 83 | 84 | In this case, `togglePerson` will actually get a new reference every time props `people` or `onToggle` will update. 85 | 86 | ## When to use it and when not to use it? 87 | 88 | You can think about it in the same way as class method. 89 | 90 | In class, if you'd have something like 91 | 92 | ```tsx 93 | 46 | 47 | ); 48 | }); 49 | 50 | render(, document.getElementById('app')); 51 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | border: 1px solid black; 3 | padding: 10px; 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-method", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "license": "MIT", 7 | "keywords": [ 8 | "react", 9 | "optimize", 10 | "memo", 11 | "performance", 12 | "callback", 13 | "reference", 14 | "ref" 15 | ], 16 | "scripts": { 17 | "build": "rimraf dist && tsc", 18 | "demo": "parcel demo/index.html", 19 | "demo:build": "rimraf demoBuild & parcel build demo/index.html -d demoBuild --no-source-maps", 20 | "demo:deploy": "yarn demo:build --public-url \"/use-method\" && gh-pages -d demoBuild" 21 | }, 22 | "homepage": "https://github.com/pie6k/use-method", 23 | "repository": { 24 | "url": "https://github.com/pie6k/use-method" 25 | }, 26 | "peerDependencies": { 27 | "react": "^16.8" 28 | }, 29 | "devDependencies": { 30 | "@types/react": "^16.9.34", 31 | "@types/react-dom": "^16.9.6", 32 | "gh-pages": "^2.2.0", 33 | "parcel": "^1.12.4", 34 | "parcel-bundler": "^1.12.4", 35 | "react": "^16.13.1", 36 | "react-dom": "^16.13.1", 37 | "rimraf": "^3.0.2", 38 | "typescript": "^3.8.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useLayoutEffect } from 'react'; 2 | 3 | type Callback = (...args: Args) => Result; 4 | 5 | /** 6 | * Create callback that keeps the same reference during entire lifecycle of the component while having access to fresh 7 | * variables values on every call 8 | * 9 | * Note: It can only be used inside callbacks like events / server responses / effects etc. 10 | * 11 | * If you want to call it during the render - you probably don't need to memoize the function or using `useCallbacl` might be more suitable 12 | */ 13 | export function useMethod( 14 | callback: Callback, 15 | ) { 16 | const lastRenderCallbackRef = useRef>(callback); 17 | 18 | const methodLikeCallback = useCallback((...args: Args) => { 19 | // perform call on version of the callback from last commited render 20 | return lastRenderCallbackRef.current(...args); 21 | }, []); 22 | 23 | /** 24 | * In concurrent mode - render method can be called multiple times without commiting the update 25 | * also - even after rendering happened - some update might get aborted and actually never be commited. 26 | * 27 | * For that reason - it's important to actually update the function only after the render is commited. 28 | * 29 | * If we do it during the render - such case is possible 30 | * 1. new render starts 31 | * - optionally render function might actually get called multiple times (side note - in react strict mode - react actually intentionally call every render function twice to break things quickly in dev mode if they're implemented incorrectly) 32 | * 2. during every render call function reference is replaced 33 | * 3. let's say render is not instantly commited (or gets aborted) 34 | * - now we have callback ref that comes from uncommited, dead render 35 | * 4. now function callback get's called before next render attempt due to some event/server response etc 36 | * 5. as it's already replaced - it's calling callback that is not commited to any render and might work incorrectly / cause really nasty bugs or actually crash the app 37 | * 38 | * When doing assignment inside layout effect 39 | * - even if render will be called multiple times without commiting - still previous commited callback ref is kept 40 | * - it means that even if some callbacks are called - they'll use proper, last working callback 41 | */ 42 | // TODO: During first render - we assing callback ref instantly which could break in concurrent mode due to above reasons. 43 | useLayoutEffect(() => { 44 | // render is commited - it's safe to update the callback 45 | lastRenderCallbackRef.current = callback; 46 | }); 47 | 48 | return methodLikeCallback; 49 | } 50 | 51 | export default useMethod; 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "lib": ["es2015", "dom"], 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "removeComments": true, 12 | "jsx": "react" 13 | }, 14 | "include": ["src/**/*.ts", "demo/**/*.ts"], 15 | "exclude": ["node_modules", "./dist", "./demo", "src/**/*.spec.ts"] 16 | } 17 | --------------------------------------------------------------------------------