reflective-bind
Advanced tools
Comparing version 0.0.4 to 0.1.0
@@ -50,2 +50,3 @@ /** | ||
let _propRegexCompiled; | ||
let _filename; | ||
@@ -68,2 +69,3 @@ let _hoistedSlug; | ||
log = "off", | ||
propRegex = undefined, | ||
@@ -84,2 +86,4 @@ // Hoisted function name prefix | ||
logger.setLevel(log); | ||
_propRegexCompiled = | ||
propRegex != null ? new RegExp(propRegex) : undefined; | ||
_filename = file.opts.filename; | ||
@@ -130,4 +134,6 @@ _hoistPath = path; | ||
JSXAttribute(path) { | ||
// Don't transform ref callbacks | ||
if (t.isJSXIdentifier(path.node.name) && path.node.name.name === "ref") { | ||
if ( | ||
t.isJSXIdentifier(path.node.name) && | ||
shouldSkipProp(path.node.name.name) | ||
) { | ||
path.skip(); | ||
@@ -158,2 +164,8 @@ } | ||
function shouldSkipProp(name) { | ||
return ( | ||
name === "ref" || (_propRegexCompiled && !_propRegexCompiled.test(name)) | ||
); | ||
} | ||
function processPath(path, state) { | ||
@@ -328,3 +340,3 @@ if (t.isVariableDeclarator(path)) { | ||
// We must detect this case, and prevent the hoisting from happening. | ||
isBindingDefinitelyBeforePath(binding, state.fnPath) && | ||
isPathDefinitelyBeforeOtherPath(binding.path, state.fnPath) && | ||
// For all constantViolations, we have to make sure they also come | ||
@@ -340,4 +352,5 @@ // before the fnPath, otherwise we will bind to the wrong value. | ||
makeMsg( | ||
`Cannot transform arrow function because the variable '${path.node | ||
.name}' is assigned to after the arrow function definition.`, | ||
`Cannot transform arrow function because the variable '${ | ||
path.node.name | ||
}' is assigned to after the arrow function definition.`, | ||
path | ||
@@ -407,28 +420,2 @@ ) | ||
function isBindingDefinitelyBeforePath(binding, path) { | ||
const isBindingFnDeclaration = t.isFunctionDeclaration(binding.path); | ||
/* istanbul ignore if */ | ||
if (isBindingFnDeclaration && binding.identifier !== binding.path.node.id) { | ||
throw new Error( | ||
"binding identifier does not match the function declaration id" | ||
); | ||
} | ||
// If the binding is a function declaration (e.g. function foo() {}), | ||
// binding.path.scope will get you the scope of the actual function, not | ||
// the scope the binding is created in. Need to do | ||
// binding.path.parentPath.scope instead. | ||
const bscope = isBindingFnDeclaration | ||
? binding.path.parentPath.scope | ||
: binding.path.scope; | ||
/* istanbul ignore if */ | ||
if (bscope !== path.scope && !isAncestorScope(bscope, path.scope)) { | ||
throw new Error( | ||
"binding's scope must be equal to or is an ancestor of the path's scope" | ||
); | ||
} | ||
return isPathDefinitelyBeforeOtherPath(binding.path, path); | ||
} | ||
// Returns true iff we are 100% sure checkPath is executed before otherPath. | ||
@@ -524,16 +511,2 @@ // https://github.com/babel/babel/blob/75808a2d14a5872472eb12ee5135faca4950d57a/packages/babel-traverse/src/path/introspection.js#L216 | ||
// Returns true iff maybeAncestorScope is an ancestor of startScope. | ||
// A scope is not an ancestor of itself: | ||
// isAncestor(someScope, someScope) === false | ||
function isAncestorScope(maybeAncestorScope, startScope) { | ||
let curScope = startScope.parent; | ||
while (curScope) { | ||
if (maybeAncestorScope === curScope) { | ||
return true; | ||
} | ||
curScope = curScope.parent; | ||
} | ||
return false; | ||
} | ||
// Return the indices in the ancestor arrays of where the common ancestor | ||
@@ -656,6 +629,6 @@ // is. If no common ancestor, return [-1, -1]. | ||
* TODO: move this blob of text out to the docs and link to it. | ||
* | ||
* | ||
* Checks if the path is part of a nested property access. | ||
* This means that it is of the form `pathNode.foo`. | ||
* | ||
* | ||
* We want to encourage developers to manually hoist these nested propery | ||
@@ -665,7 +638,7 @@ * accesses out to constant variables to reduce wasted re-renders because | ||
* function. | ||
* | ||
* | ||
* For example: | ||
* const obj = {...}; | ||
* const handleClick = () => this.setState({foo: obj.a.b.c}); | ||
* | ||
* | ||
* Is transformed to: | ||
@@ -678,3 +651,3 @@ * function _hoisted(obj) { | ||
* const handleClick = babelBind(_hoisted, this, obj); | ||
* | ||
* | ||
* In this case, the handleClick function will not be reflectively equal | ||
@@ -686,3 +659,3 @@ * whenever the `obj` references changes. It is better for the user to | ||
* const handleClick = () => this.setState({foo: c}); | ||
* | ||
* | ||
* This is transformed to: | ||
@@ -696,6 +669,6 @@ * function _hoisted(c) { | ||
* const handleClick = babelBind(_hoisted, this, c); | ||
* | ||
* | ||
* Which will be reflectively equal as long as c doesn't change value, even | ||
* if the `obj` reference changes. | ||
* | ||
* | ||
* Note that `pathNode.foo()` is ok because pulling `pathNode.foo` out and | ||
@@ -702,0 +675,0 @@ * calling `foo()` on its own does not result in the right context within |
@@ -7,8 +7,10 @@ # Reflective Bind Change Log | ||
## 0.1.0 | ||
* Add "propRegex" option to only transform matching prop names. | ||
## 0.0.4 | ||
* Add "log" option to log general transform info and warnings. | ||
* Log info about which inline functions are transformed. | ||
* Log warnings about sub-optimial code and how to fix it. | ||
## 0.0.4-rc2 | ||
* Don't transform inline functions on `ref` prop. | ||
@@ -15,0 +17,0 @@ * Don't transform arrow functions defined at the top level. |
{ | ||
"name": "reflective-bind", | ||
"version": "0.0.4", | ||
"version": "0.1.0", | ||
"description": "Eliminate wasteful re-rendering in React components caused by inline functions", | ||
@@ -5,0 +5,0 @@ "author": "Dounan Shi", |
@@ -22,3 +22,3 @@ [![Build Status](https://travis-ci.org/flexport/reflective-bind.svg?branch=master)](https://travis-ci.org/flexport/reflective-bind) | ||
*NOTE: the design goal of the plugin is to preserve the semantics of your code. Your inline functions will still create new function instances each render. The transform simply enables the equality comparison of two function instances via reflection.* | ||
_NOTE: the design goal of the plugin is to preserve the semantics of your code. Your inline functions will still create new function instances each render. The transform simply enables the equality comparison of two function instances via reflection._ | ||
@@ -29,3 +29,3 @@ Add it to the top of your plugin list in `.babelrc` (it must be run before other plugins that transform arrow functions and `bind` calls): | ||
"plugins": [ | ||
["reflective-bind/babel", {log: "debug"}], | ||
["reflective-bind/babel", {"log": "debug"}], | ||
... | ||
@@ -70,10 +70,17 @@ ] | ||
#### log (*default: off*) | ||
#### propRegex (_default: transform all props_) | ||
When specified, only transform props whose name matches the regular expression. The intended use case is to avoid transforming render callbacks, as this can lead to stale render bugs. | ||
For example, if all of your non-render callbacks are prefixed with `on`, such as `onClick`, consider using `"propRegex": "^on[A-Z].*$"`. | ||
#### log (_default: off_) | ||
Specifies the minimum level of logs to output to the console. Enabling logging at a given level also enables logging at all higher levels. | ||
- **debug** - output messages useful for debugging (e.g. which functions are transformed). | ||
- **info** - output helpful information (e.g. optimization tips). | ||
- **warn** - output warnings (e.g. which functions cannot be transformed). These can usually be fixed with some simple refactoring. | ||
- **off** - disable logging. Recommended for production. | ||
* **debug** - output messages useful for debugging (e.g. which functions are transformed). | ||
* **info** - output helpful information (e.g. optimization tips). | ||
* **warn** - output warnings (e.g. which functions cannot be transformed). These can usually be fixed with some simple refactoring. | ||
* **off** - disable logging. Recommended for production. | ||
### Dependencies | ||
@@ -101,7 +108,7 @@ | ||
fn1 === fn2 // false | ||
reflectiveEqual(fn1, fn2) // true | ||
fn1 === fn2; // false | ||
reflectiveEqual(fn1, fn2); // true | ||
const fn3 = reflectiveBind(baseFn, undefined, "world"); | ||
reflectiveEqual(fn1, fn3) // false | ||
reflectiveEqual(fn1, fn3); // false | ||
``` | ||
@@ -112,4 +119,4 @@ | ||
```js | ||
reflectiveEqual(1, 1) // false | ||
reflectiveEqual(baseFn, baseFn) // false | ||
reflectiveEqual(1, 1); // false | ||
reflectiveEqual(baseFn, baseFn); // false | ||
``` | ||
@@ -145,3 +152,3 @@ | ||
- Inline arrow functions: | ||
* Inline arrow functions: | ||
@@ -151,7 +158,7 @@ ```jsx | ||
const msg = "Hello " + props.user.name.first; | ||
return <PureChild onClick={() => alert(msg)} /> | ||
return <PureChild onClick={() => alert(msg)} />; | ||
} | ||
``` | ||
- `Function.prototype.bind`: | ||
* `Function.prototype.bind`: | ||
@@ -161,7 +168,7 @@ ```jsx | ||
const handleClick = props.callback.bind(undefined, "yay"); | ||
return <PureChild onClick={handleClick} /> | ||
return <PureChild onClick={handleClick} />; | ||
} | ||
``` | ||
- Multiple assignments / reassignments: | ||
* Multiple assignments / reassignments: | ||
@@ -171,3 +178,3 @@ ```jsx | ||
let handleClick = () => {...}; | ||
if (...) { | ||
@@ -183,3 +190,3 @@ handleClick = () => {...}; | ||
- Ternary expressions: | ||
* Ternary expressions: | ||
@@ -196,14 +203,13 @@ ```jsx | ||
- For maximum optimization, avoid accessing nested attributes in your arrow function. Prefer to pull the nested value out to a const and close over it in your arrow function. | ||
* For maximum optimization, avoid accessing nested attributes in your arrow function. Prefer to pull the nested value out to a const and close over it in your arrow function. | ||
```jsx | ||
function MyComponent(props) { | ||
// PureChild will re-render whenever `props` changes (bad) | ||
const badHandleClick = () => alert(props.user.name.first); | ||
const badHandleClick = () => alert(props.user.name.first); | ||
const firstName = props.user.name.first; | ||
// Now, PureChild will only re-render when firstName changes (good) | ||
const goodHandleClick = () => alert(firstName); | ||
return ( | ||
@@ -222,3 +228,3 @@ <div> | ||
- Your arrow function should not close over variables whose value is set after the arrow function. | ||
* Your arrow function should not close over variables whose value is set after the arrow function. | ||
@@ -228,3 +234,3 @@ ```jsx | ||
let foo = 1; | ||
const badHandleClick = () => { | ||
@@ -235,10 +241,10 @@ // Referencing `foo`, which is reassigned after this arrow function, will | ||
}; | ||
foo = 2; | ||
return <PureChild onClick={badHandleClick} /> | ||
return <PureChild onClick={badHandleClick} />; | ||
} | ||
``` | ||
- Your arrow function must be defined inline the JSX, or at most 1 reference away. | ||
* Your arrow function must be defined inline the JSX, or at most 1 reference away. | ||
@@ -251,13 +257,13 @@ ```jsx | ||
const badHandleClick = fn; | ||
// This arrow function will be transformed since `goodHandleClick` is | ||
// referenced directly in the JSX. | ||
const goodHandleClick = () => {...}; | ||
return ( | ||
<div> | ||
<PureChild onClick={badHandleClick} /> | ||
<PureChild onClick={goodHandleClick} /> | ||
{/* This will be optimized since it is defined directly in the JSX */} | ||
@@ -264,0 +270,0 @@ <PureChild onClick={() => {...}} /> |
260
53819
986