├── LICENSE ├── README.md ├── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tony 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Render Debugger 2 | ============ 3 | A visual way to see what is (re)rendering and why. 4 | 5 | Decorator/higher-order function version ported from 6 | 7 | [Learn more about the experimental decorator syntax](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy#why-legacy). 8 | 9 | Features 10 | -------- 11 | - Shows when component is being mounted or updated by highlighting (red for mount, yellow for update) 12 | - Shows render count for each component instance 13 | - Shows individual render log for each component instance 14 | 15 | Installation 16 | ------------ 17 | 18 | ```sh 19 | npm install react-render-debugger 20 | ``` 21 | 22 | Usage 23 | ----- 24 | Import and apply to any React component you want to start monitoring: 25 | 26 | ```js 27 | import React, { Component } from 'react'; 28 | import debugRender from 'react-render-debugger'; 29 | 30 | // Use with the decorator syntax (experimental) 31 | @debugRender 32 | class DecoratedComponent extends Component { 33 | render () { 34 | // ... 35 | } 36 | } 37 | 38 | // Or simply passing the component to the function 39 | class PlainComponent extends Component { 40 | render () { 41 | // ... 42 | } 43 | } 44 | 45 | const WrappedPlainComponent = debugRender(PlainCompoent); 46 | ``` 47 | Component will show up with a blue border box when being monitored. 48 | 49 | 50 | Demo 51 | ---- 52 | See a demo page: 53 | 54 | Similar libraries 55 | ----------------- 56 | 57 | * [mobx-react-devtools](https://github.com/mobxjs/mobx-react-devtools) 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | 6 | function visualizeRender (component) { 7 | var originalComponentDidMount = component.prototype.componentDidMount; 8 | var originalComponentDidUpdate = component.prototype.componentDidUpdate; 9 | var originalComponentWillUnmount = component.prototype.componentWillUnmount; 10 | 11 | component.prototype.UPDATE_RENDER_LOG_POSITION_TIMEOUT_MS = 500; 12 | component.prototype.MAX_LOG_LENGTH = 20; 13 | component.prototype.STATE_CHANGES = { 14 | MOUNT: 'mount', 15 | UPDATE: 'update' 16 | }; 17 | component.prototype.styling = { 18 | renderLog: { 19 | color: 'rgb(85, 85, 85)', 20 | fontFamily: '\'Helvetica Neue\', Arial, Helvetica, sans-serif', 21 | fontSize: '14px', 22 | lineHeight: '18px', 23 | background: 'linear-gradient(#fff, #ccc)', 24 | boxShadow: '0 2px 12px rgba(0,0,0,0.5)', 25 | textShadow: '0 1px 0 #fff', 26 | borderRadius: '7px', 27 | position: 'absolute', 28 | maxWidth: '70%', 29 | padding: '5px 10px', 30 | zIndex: '10000' 31 | }, 32 | renderLogDetailNotes: { 33 | color: 'red', 34 | textAlign: 'center' 35 | }, 36 | elementHighlightMonitor: { 37 | outline: '1px solid rgba(47, 150, 180, 1)' 38 | }, 39 | elementHighlightMount: { 40 | outline: '3px solid rgba(197, 16, 12, 1)' 41 | }, 42 | elementHighlightUpdate: { 43 | outline: '3px solid rgba(197, 203, 1, 1)' 44 | } 45 | }; 46 | 47 | component.prototype.componentDidMount = function(){ 48 | // Create empty state object if not defined 49 | if (!this.state) { 50 | this.state = {}; 51 | } 52 | 53 | // Reset the logs 54 | this._resetRenderLog(); 55 | 56 | // Record initial mount 57 | this.addToRenderLog(this.state, 'Initial Render'); 58 | 59 | // Build the monitor node 60 | this._buildRenderLogNode(); 61 | 62 | // Highlight the initial mount 63 | this._highlightChange(this.STATE_CHANGES.MOUNT); 64 | 65 | // Set the watch to update log position 66 | this._updateRenderLogPositionTimeout = setInterval( 67 | this._updateRenderLogPosition.bind(this), this.UPDATE_RENDER_LOG_POSITION_TIMEOUT_MS 68 | ); 69 | 70 | if (typeof originalComponentDidMount === 'function') { 71 | originalComponentDidMount.call(this); 72 | } 73 | }; 74 | 75 | component.prototype.componentDidUpdate = function(prevProps, prevState){ 76 | // Get the changes in state and props 77 | this._getReasonForReRender(prevProps, prevState); 78 | 79 | // Update the render log 80 | this._updateRenderLogNode(); 81 | 82 | // Highlight the update 83 | this._highlightChange(this.STATE_CHANGES.UPDATE); 84 | 85 | if (typeof originalComponentDidUpdate === 'function') { 86 | originalComponentDidUpdate.call(this, arguments); 87 | } 88 | }; 89 | 90 | component.prototype.componentWillUnmount = function (){ 91 | // Remove the monitor node 92 | this._removeRenderLogNode(); 93 | 94 | // Clear the update position timeout 95 | clearInterval(this._updateRenderLogPositionTimeout); 96 | 97 | if (typeof originalComponentWillUnmount === 'function') { 98 | originalComponentWillUnmount.call(this); 99 | } 100 | }; 101 | 102 | /* 103 | * Reset the logs 104 | * @return void 105 | */ 106 | component.prototype._resetRenderLog = function(){ 107 | this.state.renderLog = []; 108 | this.state.renderCount = 1; 109 | }; 110 | 111 | component.prototype._applyCSSStyling = function (node, styles) { 112 | Object.keys(styles).forEach(function(className) { 113 | node.style[className] = styles[className]; 114 | }); 115 | }; 116 | 117 | /* 118 | * Build the renderLog node, add it to the body and assign it's position 119 | * based on the monitored component 120 | * @return void 121 | */ 122 | component.prototype._buildRenderLogNode = function(){ 123 | var renderLogContainer = document.createElement('div'), 124 | renderLogRenderCountNode = document.createElement("div"), 125 | renderLogDetailContainer = document.createElement("div"), 126 | renderLogNotesNode = document.createElement("div"), 127 | renderLogDetailNode = document.createElement("div"); 128 | 129 | renderLogContainer.className = 'renderLog'; 130 | 131 | // Apply styling 132 | this._applyCSSStyling(renderLogContainer, this.styling.renderLog) 133 | 134 | // Attach the click handler for toggling the detail log 135 | renderLogContainer.addEventListener('click', function(){ 136 | 137 | // Show the detail Log 138 | if (renderLogRenderCountNode.style.display === 'none') { 139 | renderLogRenderCountNode.style.display = 'block'; 140 | renderLogDetailContainer.style.display = 'none'; 141 | renderLogContainer.style.zIndex = '10000'; 142 | // Hide it 143 | } else { 144 | renderLogRenderCountNode.style.display = 'none'; 145 | renderLogDetailContainer.style.display = 'block'; 146 | renderLogContainer.style.zIndex = '10001'; 147 | } 148 | }); 149 | 150 | renderLogRenderCountNode.className = 'renderLogCounter'; 151 | renderLogRenderCountNode.innerText = 1; 152 | 153 | renderLogDetailContainer.style.display = 'none'; 154 | renderLogDetailNode.innerText = ''; 155 | 156 | if (this.shouldComponentUpdate) { 157 | renderLogNotesNode.innerText = 'NOTE: This component uses a custom shouldComponentUpdate(), so the results above are purely informational'; 158 | } 159 | 160 | this._applyCSSStyling(renderLogNotesNode, this.styling.renderLogDetailNotes); 161 | 162 | renderLogDetailContainer.appendChild(renderLogDetailNode); 163 | renderLogDetailContainer.appendChild(renderLogNotesNode); 164 | 165 | renderLogContainer.appendChild(renderLogRenderCountNode); 166 | renderLogContainer.appendChild(renderLogDetailContainer); 167 | 168 | this.renderLogContainer = renderLogContainer; 169 | this.renderLogDetail = renderLogDetailNode; 170 | this.renderLogNotes = renderLogNotesNode; 171 | this.renderLogRenderCount = renderLogRenderCountNode; 172 | 173 | // Append to the body 174 | document.getElementsByTagName('body')[0].appendChild(renderLogContainer); 175 | 176 | // Set initial position 177 | this._updateRenderLogPosition(); 178 | 179 | // 180 | this._updateRenderLogNode(); 181 | }; 182 | 183 | /* 184 | * Update the render log position based on its parent position 185 | * @return void 186 | */ 187 | component.prototype._updateRenderLogPosition = function(){ 188 | var parentNode = ReactDOM.findDOMNode(this), 189 | parentNodeRect = parentNode && parentNode.getBoundingClientRect(); 190 | 191 | if (this.renderLogContainer && parentNodeRect) { 192 | this.renderLogContainer.style.top = (window.pageYOffset + parentNodeRect.top) + 'px'; 193 | this.renderLogContainer.style.left = (parentNodeRect.left) + 'px'; 194 | } 195 | }; 196 | 197 | /* 198 | * Update the render log count and details 199 | * @return void 200 | */ 201 | component.prototype._updateRenderLogNode = function() { 202 | var logFragment = document.createDocumentFragment(); 203 | 204 | if (this.renderLogRenderCount) { 205 | this.renderLogRenderCount.innerText = (this.state.renderCount - 1 ); 206 | } 207 | 208 | if (this.renderLogDetail) { 209 | this.renderLogDetail.innerHTML = ''; 210 | for (var i = 0; i < this.state.renderLog.length; i++){ 211 | var item = document.createElement('div'); 212 | item.innerText = this.state.renderLog[i]; 213 | logFragment.appendChild(item); 214 | } 215 | 216 | this.renderLogDetail.appendChild(logFragment); 217 | } 218 | //this.state.renderCount++; 219 | }; 220 | 221 | /* 222 | * Remove the render log node from the body 223 | * @return void 224 | */ 225 | component.prototype._removeRenderLogNode = function() { 226 | if (this.renderLogContainer) { 227 | document.getElementsByTagName('body')[0].removeChild(this.renderLogContainer); 228 | } 229 | }; 230 | 231 | /* 232 | * Add a detail message to the render log and update the count 233 | * @param object nextState - The most current state of the component 234 | * @param String message 235 | * @return void 236 | */ 237 | component.prototype.addToRenderLog = function(state, message) { 238 | state.renderLog.unshift(state.renderCount + ') ' + message); 239 | state.renderCount++; 240 | 241 | // Trim the log 242 | state.renderLog.splice(this.MAX_LOG_LENGTH, 1); 243 | }; 244 | 245 | 246 | /* 247 | * Get the changes made to props or state. In the event this component has its own 248 | * shouldComponentUpdate, don't do 249 | * anything 250 | * @param object prevProps 251 | * @param object prevState 252 | * @return boolean 253 | */ 254 | component.prototype._getReasonForReRender = function(prevProps, prevState) { 255 | var nextState = this.state, 256 | nextProps = this.props, 257 | key; 258 | 259 | for (key in nextState){ 260 | if (nextState.hasOwnProperty(key) && nextState[key] !== prevState[key]){ 261 | if (typeof nextState[key] === 'object') { 262 | return this.addToRenderLog(this.state, 'this.state['+key+'] changed'); 263 | } else { 264 | return this.addToRenderLog(this.state, 265 | 'this.state['+key+'] changed: \'' + prevState[key] + '\' => \'' + nextState[key] + '\''); 266 | } 267 | 268 | } 269 | } 270 | 271 | for (key in nextProps) { 272 | if (nextProps.hasOwnProperty(key) && nextProps[key] !== prevProps[key]) { 273 | if (typeof nextProps[key] === 'object') { 274 | return this.addToRenderLog(this.state, 'this.props['+key+'] changed'); 275 | } else { 276 | return this.addToRenderLog(this.state, 277 | 'this.props['+key+'] changed: \'' + prevProps[key] + '\' => \'' + nextProps[key] + '\''); 278 | } 279 | } 280 | } 281 | 282 | return this.addToRenderLog(this.state, 'unknown reason for update, possibly from forceUpdate()'); 283 | }; 284 | 285 | /* 286 | * Highlight any change by adding an animation style to the component DOM node 287 | * @param String change - The type of change being made to the node 288 | * @return void 289 | */ 290 | component.prototype._highlightChange = function(change) { 291 | var parentNode = ReactDOM.findDOMNode(this), 292 | ANIMATION_DURATION = 500; 293 | 294 | if (parentNode) { 295 | parentNode.style.boxSizing = 'border-box'; 296 | 297 | window.requestAnimationFrame(function () { 298 | // Immediately show the border 299 | parentNode.style.transition = 'outline 0s'; 300 | if (change === this.STATE_CHANGES.MOUNT) { 301 | parentNode.style.outline = this.styling.elementHighlightMount.outline; 302 | } else { 303 | parentNode.style.outline = this.styling.elementHighlightUpdate.outline; 304 | } 305 | 306 | // Animate the border back to monitored color 307 | window.requestAnimationFrame(function () { 308 | parentNode.style.outline = this.styling.elementHighlightMonitor.outline; 309 | parentNode.style.transition = 'outline '+ANIMATION_DURATION+'ms linear'; 310 | }.bind(this)); 311 | }.bind(this)); 312 | } 313 | }; 314 | 315 | component.renderLogContainer = null; 316 | component.renderLogDetail = null; 317 | component.renderLogRenderCount = null; 318 | component._updateRenderLogPositionTimeout = null; 319 | 320 | return component; 321 | } 322 | 323 | module.exports = visualizeRender; 324 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-render-debugger", 3 | "version": "1.0.2", 4 | "description": "Render debugger for React", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/marcin-mazurek/react-render-debugger.git" 10 | }, 11 | "author": { 12 | "name": "Marcin Mazurek", 13 | "email": "marcin@mazurek.pro" 14 | }, 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/marcin-mazurek/react-render-debugger/issues" 18 | }, 19 | "homepage": "https://github.com/marcin-mazurek/react-render-debugger", 20 | "tags": [ 21 | "react", 22 | "render", 23 | "visualizer", 24 | "decorator", 25 | "es7" 26 | ], 27 | "keywords": [ 28 | "react", 29 | "render", 30 | "visualizer", 31 | "decorator", 32 | "es7" 33 | ], 34 | "peerDependencies": { 35 | "react": ">=0.14.0", 36 | "react-dom": ">=0.14.0" 37 | }, 38 | "readmeFilename": "README.md" 39 | } 40 | --------------------------------------------------------------------------------