├── .gitignore ├── .babelrc ├── package.json ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | .idea/ -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", "react", "stage-2" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-transform-react-binding", 3 | "version": "0.1.0", 4 | "description": "Babel transform to optimise function binding in react render methods", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "babel src/ -d lib/ --source-maps", 9 | "build:watch": "babel src/ --out-dir lib/ --source-maps --watch" 10 | }, 11 | "keywords": [ 12 | "babel", 13 | "react", 14 | "bind" 15 | ], 16 | "author": "Chris Pearce ", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "babel-cli": "^6.6.5", 20 | "babel-plugin-transform-runtime": "^6.6.0", 21 | "babel-preset-es2015": "^6.6.0", 22 | "babel-preset-react": "^6.5.0", 23 | "babel-preset-stage-2": "^6.5.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 😱 Experimental! 😱 3 |

4 | 5 | # babel-plugin-transform-react-binding 6 | 7 | Automatically memoize function binding in your react render methods. 8 | 9 | ## What? 10 | 11 | So you want to bind (bind context OR partially apply args) functions in your React 12 | render method? Psych! You're going to break the purity of your next components since 13 | you're *recreating* those functions on *every* render call. To fix this you need to 14 | do context binding elsewhere, pass extra redundant props to caller or muck about with 15 | cumbersome boilerplate. 16 | 17 | This plugin takes care of this for you. Write your `func.bind(this, whatever, arg)` calls. Use `event => handler(some, args, event)` 18 | freely. 19 | 20 | Just, stop worrying! 21 | 22 | We will transparently bind and memoize these behind the scenes. [[Example]](https://astexplorer.net/#/JDXJSoobah/3) 23 | 24 | > You can read more about the function binding problem [here](https://medium.com/@roman01la/avoid-partial-application-in-react-components-3c9e36d7f735#.6188frv1b). 25 | 26 | ## Installation 27 | 28 | ```sh 29 | $ npm install babel-plugin-transform-react-binding --save-dev 30 | ``` 31 | 32 | We also leave the memoize implementation up to you allowing you to specify via the 33 | `memoizeModule` option (see [#Options](#options)). By default this uses the 34 | [lru-memoize](https://www.npmjs.com/package/lru-memoize) package so you need to install 35 | that too to get the default working. 36 | 37 | ```sh 38 | $ npm install lru-memoize --save 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Via `.babelrc` (Recommended) 44 | 45 | **.babelrc** 46 | 47 | ```json 48 | { 49 | "plugins": ["transform-react-binding"] 50 | } 51 | ``` 52 | 53 | ### Via CLI 54 | 55 | ```sh 56 | $ babel --plugins transform-react-binding script.js 57 | ``` 58 | 59 | ### Via Node API 60 | 61 | ```javascript 62 | require("babel-core").transform("code", { 63 | plugins: ["babel-plugin-transform-react-binding"] 64 | }); 65 | 66 | 67 | 68 | ``` 69 | 70 | ### Options 71 | 72 | - `cacheLimit = 500` - number of entries to keep in memoize cache PER COMPONENT 73 | - `memoizeModule = 'lru-cache'` - module exporting memoize implementation 74 | 75 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * New babel plugin optimising function binding in React render methods 3 | * @note replaces `func.bind(this, value)` calls 4 | * @note replaces `() => func(value)` calls 5 | */ 6 | export default function({types: T, template}) { 7 | const DEFAULT_CACHE_SIZE = 500; 8 | const DEFAULT_MEMOIZE_MODULE = 'lru-memoize'; 9 | 10 | const memoizeBindTemplate = template(` 11 | const FUNC_IDENTIFIER = MEMOIZE_IDENTIFIER(CACHE_SIZE)(function(func, context, ...args) { 12 | return func.bind(context, ...args); 13 | }); 14 | `); 15 | 16 | const importTemplate = template(` 17 | const MEMOIZE_IDENTIFIER = require(MEMOIZE_MODULE); 18 | `); 19 | 20 | // traversal trackers 21 | let methodHasBindCall = false; 22 | let moduleHasBindCall = false; 23 | let hoistedFuncs = []; 24 | 25 | /** 26 | * Does a given path contain react elements? 27 | * Eg. JSX or React.createElement() calls 28 | * @todo support React.createElement() 29 | */ 30 | function containsReactElements(path) { 31 | if (T.isJSXElement(path)) { 32 | return true; 33 | } 34 | 35 | let doesContainJSX = false; 36 | 37 | path.traverse({ 38 | JSXElement(jsxPath) { 39 | doesContainJSX = true; 40 | jsxPath.stop(); 41 | } 42 | }); 43 | 44 | return doesContainJSX; 45 | } 46 | 47 | // Find and optimise .bind() calls and () => func expressions 48 | const callVisitor = { 49 | CallExpression(path) { 50 | if ( 51 | T.isMemberExpression(path.node.callee) 52 | && T.isIdentifier(path.node.callee.property, {name: 'bind'}) 53 | ) { 54 | methodHasBindCall = moduleHasBindCall = true; 55 | 56 | // replace expression with optimised bind call 57 | path.replaceWith(T.callExpression( 58 | this.bindFuncIdentifier, 59 | [path.node.callee.object, ...path.node.arguments] 60 | )); 61 | } 62 | }, 63 | ArrowFunctionExpression(path) { 64 | methodHasBindCall = moduleHasBindCall = true; 65 | 66 | const hoistedFuncIdentifier = this.insertScope.generateUidIdentifier('hoistedFunc'); 67 | const referencedIdentifiers = []; 68 | 69 | // extract referenced identifiers so we can bind them 70 | path.get('body').traverse({ 71 | Identifier(idPath) { 72 | const binding = idPath.scope.getBinding(idPath.node.name); 73 | const local = binding 74 | ? binding.scope.uid === path.scope.uid 75 | : false; 76 | 77 | if ( 78 | // not locally bound 79 | !local && 80 | // Don't extract nested identifiers from member expressions with 81 | // the exception of computed properties 82 | ( 83 | !T.isMemberExpression(idPath.parentPath.node) || 84 | ( 85 | // root identifier in member expression 86 | idPath.parentPath.node.object.name === idPath.node.name || 87 | // identifier within computed property 88 | idPath.parentPath.node.computed === true 89 | ) 90 | ) 91 | ) { 92 | referencedIdentifiers.push(idPath.node); 93 | } 94 | } 95 | }); 96 | 97 | // create a function which can be hoisted from the render function 98 | const funcBody = T.isBlockStatement(path.node.body) 99 | ? path.node.body 100 | : T.blockStatement([ 101 | T.returnStatement(path.node.body) 102 | ]); 103 | const hoistedFunc = T.functionDeclaration( 104 | hoistedFuncIdentifier, 105 | [...referencedIdentifiers, ...path.node.params], 106 | funcBody 107 | ); 108 | 109 | // replace expression with optimized bind call 110 | path.replaceWith(T.callExpression( 111 | this.bindFuncIdentifier, 112 | [hoistedFuncIdentifier, T.thisExpression(), ...referencedIdentifiers] 113 | )); 114 | 115 | hoistedFuncs.push(hoistedFunc); 116 | } 117 | }; 118 | 119 | /** Traverse and optimise a react render function */ 120 | function traverseRenderFunc(path, {opts: {cacheSize = DEFAULT_CACHE_SIZE} = {}}) { 121 | // find the appropiate location and scope for code insertion 122 | let insertBeforePath = path; 123 | while (!T.isProgram(insertBeforePath.parentPath)) { 124 | insertBeforePath = insertBeforePath.parentPath; 125 | } 126 | const insertScope = insertBeforePath.scope; 127 | 128 | // Create a new memoized function binder 129 | const bindFuncIdentifier = insertScope.generateUidIdentifier('bindRenderFunc'); 130 | const bindFunc = memoizeBindTemplate({ 131 | FUNC_IDENTIFIER: bindFuncIdentifier, 132 | CACHE_SIZE: T.numericLiteral(cacheSize), 133 | MEMOIZE_IDENTIFIER: this.memoizeIdentifier 134 | }); 135 | 136 | // look for calls/binds to optimized! 137 | path.traverse(callVisitor, {bindFuncIdentifier, insertScope}); 138 | 139 | // insert memoize bind function and hosited functions if there are any 140 | if (methodHasBindCall) { 141 | insertBeforePath.insertBefore(bindFunc); 142 | methodHasBindCall = false; 143 | 144 | for (let i = 0; i < hoistedFuncs.length; ++i) { 145 | insertBeforePath.insertBefore(hoistedFuncs[i]); 146 | } 147 | hoistedFuncs = []; 148 | } 149 | } 150 | 151 | // look for react render methods/functions 152 | const renderMethodVisitor = { 153 | ClassMethod(path, context) { 154 | if (T.isIdentifier(path.node.key, {name: 'render'}) && containsReactElements(path)) { 155 | traverseRenderFunc.call(this, path, context); 156 | } 157 | }, 158 | ArrowFunctionExpression(path, context) { 159 | if (containsReactElements(path)) { 160 | traverseRenderFunc.call(this, path, context); 161 | } 162 | }, 163 | FunctionDeclaration(path, context) { 164 | if (containsReactElements(path)) { 165 | traverseRenderFunc.call(this, path, context); 166 | } 167 | }, 168 | FunctionExpression(path, context) { 169 | if (containsReactElements(path)) { 170 | traverseRenderFunc.call(this, path, context); 171 | } 172 | } 173 | }; 174 | 175 | const visitor = { 176 | // Top-level `Program` traversal due to ordering issues 177 | // See https://phabricator.babeljs.io/T6730 for info 178 | Program(path, {opts: {memoizeModule = DEFAULT_MEMOIZE_MODULE} = {}}) { 179 | const memoizeIdentifier = path.scope.generateUidIdentifier('memoize'); 180 | path.traverse(renderMethodVisitor, {memoizeIdentifier}); 181 | 182 | // add memoize import if needed 183 | if (moduleHasBindCall) { 184 | const imprt = importTemplate({ 185 | MEMOIZE_IDENTIFIER: memoizeIdentifier, 186 | MEMOIZE_MODULE: T.stringLiteral(memoizeModule) 187 | }); 188 | path.node.body.splice(0, 0, imprt); 189 | moduleHasBindCall = false; 190 | } 191 | } 192 | }; 193 | 194 | return {visitor}; 195 | }; 196 | --------------------------------------------------------------------------------