|
| 1 | +# babel-plugin-react-transform |
| 2 | + |
| 3 | +This Babel plugin wraps all React components into arbitrary transforms written by the community. |
| 4 | +In other words, **it lets you instrument React components** in any custom way. |
| 5 | + |
| 6 | +Such transforms can do a variety of things: |
| 7 | + |
| 8 | +* catch errors inside `render()` and render them in a [red screen of death](https://github.com/KeywordBrain/redbox-react); |
| 9 | +* enable hot reloading a la [React Hot Loader](https://github.com/gaearon/react-hot-loader); |
| 10 | +* render an inline prop inspector a la [React DevTools](https://github.com/facebook/react-devtools); |
| 11 | +* highlight parts of the screen when components update, |
| 12 | +* etc. |
| 13 | + |
| 14 | +The limit is your imagination and the time you feel compelled to spend on writing these transforms. |
| 15 | +Time will show whether it is an amazing, or a terrible idea. |
| 16 | + |
| 17 | +## Installation |
| 18 | + |
| 19 | +First, install the plugin: |
| 20 | + |
| 21 | +``` |
| 22 | +npm install --save-dev babel-plugin-react-transform |
| 23 | +``` |
| 24 | + |
| 25 | +Then, install the transforms you’re interested in: |
| 26 | + |
| 27 | +``` |
| 28 | +# Okay, these don't actually exist yet but I'll publish them soon |
| 29 | +npm install --save-dev react-transform-webpack-hmr |
| 30 | +npm install --save-dev react-transform-catch-errors |
| 31 | +``` |
| 32 | + |
| 33 | +Then edit your `.babelrc` to include `extra.babel-plugin-react-transform`. |
| 34 | +It must be an array of the transforms you want to use: |
| 35 | + |
| 36 | +```js |
| 37 | +{ |
| 38 | + "stage": 0, |
| 39 | + "plugins": [ |
| 40 | + "babel-plugin-react-transform" |
| 41 | + ], |
| 42 | + "extra": { |
| 43 | + // must be defined and be an array |
| 44 | + "babel-plugin-react-transform": [{ |
| 45 | + // can be an NPM module name or a local path |
| 46 | + "target": "react-transform-webpack-hmr", |
| 47 | + // will be available as options.imports to the transform |
| 48 | + "imports": ["react"], |
| 49 | + // will be available as options.locals to the transform |
| 50 | + "locals": ["module"] |
| 51 | + }, { |
| 52 | + // can be an NPM module name or a local path |
| 53 | + "target": "react-transform-catch-errors", |
| 54 | + // will be available as options.imports to the transform |
| 55 | + "imports": ["react", "redbox-react"] |
| 56 | + }, { |
| 57 | + // can be an NPM module name or a local path |
| 58 | + "target": "./src/my-custom-transform" |
| 59 | + }] |
| 60 | + } |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +As you can see each transform, apart from the `target` field where you write it name, also has `imports` and `locals` fields. You should consult the docs of each individual transform to learn which `imports` and `locals` it might need, and how it uses them. You probably already guessed that this is just a way to inject local variables (like `module`) or dependencies (like `react`) into the transforms that need them. |
| 65 | + |
| 66 | +## Writing a Transform |
| 67 | + |
| 68 | +It’s not hard to write a custom transform! First, make sure you call your NPM package `react-transform-*` so we have uniform naming across the transforms. The only thing you should export from your transform module is a function. |
| 69 | + |
| 70 | +```js |
| 71 | +export default function myTransform() { |
| 72 | + // ¯\_(ツ)_/¯ |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +This function should *return another function*: |
| 77 | + |
| 78 | +```js |
| 79 | +export default function myTransform() { |
| 80 | + return function wrap(ReactClass) { |
| 81 | + // ¯\_(ツ)_/¯ |
| 82 | + return ReactClass; |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +As you can see, you’ll receive `ReactClass` as a parameter. It’s up to you to do something with it: monkeypatch its methods, create another component with the same prototype and a few different methods, wrap it into a higher-order component, etc. Be creative! |
| 88 | + |
| 89 | +```js |
| 90 | +export default function logAllUpdates() { |
| 91 | + return function wrap(ReactClass) { |
| 92 | + const displayName = // ¯\_(ツ)_/¯ |
| 93 | + const originalComponentDidUpdate = ReactClass.prototype.componentDidUpdate; |
| 94 | + |
| 95 | + ReactClass.prototype.componentDidUpdate = function componentDidUpdate() { |
| 96 | + console.info(`${displayName} updated:`, this.props, this.state); |
| 97 | + |
| 98 | + if (originalComponentDidUpdate) { |
| 99 | + originalComponentDidUpdate.apply(this, arguments); |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + return ReactClass; |
| 104 | + } |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +Oh, how do I get `displayName`? |
| 109 | +Actually, we give your transformation function a single argument called `options`. Yes, `options`: |
| 110 | + |
| 111 | +```js |
| 112 | +export default function logAllUpdates(options) { |
| 113 | +``` |
| 114 | +
|
| 115 | +It contains some useful data. For example, your `options` could look like this: |
| 116 | +
|
| 117 | +```js |
| 118 | +{ |
| 119 | + // the file being processed |
| 120 | + filename: '/Users/dan/p/my-projects/src/App.js', |
| 121 | + // remember that "imports" .babelrc option? |
| 122 | + imports: [React], |
| 123 | + // remember that "locals" .babelrc option? |
| 124 | + locals: [module], |
| 125 | + // all components declared in the current file |
| 126 | + components: { |
| 127 | + $_MyComponent: { |
| 128 | + // with their displayName when available |
| 129 | + displayName: 'MyComponent' |
| 130 | + }, |
| 131 | + $_SomeOtherComponent: { |
| 132 | + displayName: 'SomeOtherComponent', |
| 133 | + // and telling whether they are defined inside a function |
| 134 | + isInFunction: true |
| 135 | + } |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | +
|
| 140 | +Of course, you might not want to use *all* options, but isn’t it nice to know that you have access to them in the top scope—which means before the component definitions actually run? (Hint: a hot reloading plugin might use this to decide whether a module is worthy of reloading, even if it contains an error and no React components have yet been wrapped because of it.) |
| 141 | +
|
| 142 | +So, to retrieve the `displayName` (or `isInFunction`, when available), use the `options` parameter *and* the second `uniqueId` parameter given to the inner function after `ReactClass`: |
| 143 | +
|
| 144 | +```js |
| 145 | +export default function logAllUpdates(options) { |
| 146 | + return function wrap(ReactClass, uniqueId) { |
| 147 | + const displayName = options.components[uniqueId].displayName || '<Unknown>'; |
| 148 | +``` |
| 149 | +
|
| 150 | +This is it! |
| 151 | +
|
| 152 | +Sure, it’s a slightly contrived example, as you can grab `ReactClass.displayName` just fine, but it illustrates a point: you have information about all of the components inside a file before that file executes, which is *very* handy for some transformations. |
| 153 | +
|
| 154 | +Here is the complete code for this example transformation function: |
| 155 | +
|
| 156 | +```js |
| 157 | +export default function logAllUpdates(options) { |
| 158 | + return function wrap(ReactClass, uniqueId) { |
| 159 | + const displayName = options.components[uniqueId].displayName || '<Unknown>'; |
| 160 | + const originalComponentDidUpdate = ReactClass.prototype.componentDidUpdate; |
| 161 | + |
| 162 | + ReactClass.prototype.componentDidUpdate = function componentDidUpdate() { |
| 163 | + console.info(`${displayName} updated:`, this.props, this.state); |
| 164 | + |
| 165 | + if (originalComponentDidUpdate) { |
| 166 | + originalComponentDidUpdate.apply(this, arguments); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + return ReactClass; |
| 171 | + } |
| 172 | +} |
| 173 | +``` |
| 174 | +
|
| 175 | +Now go ahead and write your own! |
| 176 | +Don’t forget to tag it with `react-transform` keyword on npm. |
| 177 | +
|
| 178 | +## License |
| 179 | +
|
| 180 | +MIT |
0 commit comments