13

I am updating a part of a page via a standard this.setState mechanism. I want to grab a hold of a elements that has been changed on a web page and provide a visual feedback to a user.

Let's say we have a component RichText that gets a data props. To render rich text it will delegate render to smaller components like Paragraph, Header, BulletPoints, Text, etc. The final result is a properly rendered rich text.

Later data props change (e.g. socket push). As a result of that Paragraphs can be added, or text changed, or things could move around. I want to provide a visual feedback to a user by simply highlighting HTML nodes that were changed.

In a nutshell I want to achieve what Chrome inspector is showing when you are looking at HTML tree. It blinks DOM changes.

ReactJS knows what was changed. Ideally I would like to get an access to that knowledge.

While smaller Components like Paragraph could be responsible for highlighting a difference within themselves, I don't think they have enough of a knowledge of the outside world to make it work as expected.

Format (simplified version)

{
  content: [{
    type: 'Document',
    content: [{
      type: 'Paragraph',
      content: [{
        type: 'Text', 
        text: 'text text'
      }, {
        type: 'Reference', 
        content: 'text text'
      },
    ]}, {
        type: 'BulletPoints', 
        content: [{
          type: 'ListEntry', content: [{
            type: 'Paragraph', content: [{
              type: 'Text', 
              text: 'text text'
            }, {
              type: 'Reference', 
              content: 'text text'
            }]
          }]
        }]
      }]

My current solution

I have a top level Component that knows how to render the entire Document by delegating job to other components. I have a live version HOC of it: LiveDocument that is responsible for a change visualization.

So I capture DOM before setState and after setState. Then I am using HtmlTreeWalker to spot a first difference (ignoring certain elements as I walk the tree).

John Slegers
  • 45,213
  • 22
  • 199
  • 169
Mykola Golubyev
  • 57,943
  • 15
  • 89
  • 102
  • I understand your problem, but I't will be nice to have a code example to give you one accurate possible solution. – damianfabian Feb 01 '17 at 06:26
  • Closest to the ReactJS hidden utility is a MutationObserver for now. – Mykola Golubyev Feb 07 '17 at 02:29
  • I gave the bounty to the first mentioning of MutationObserver as it was a solution I didn't have in my toolkit. – Mykola Golubyev Feb 07 '17 at 22:34
  • @MykolaGolubyev Hey! You probably saw my [example](http://codepen.io/cn007b/pen/qRoJwO?editors=0010) where I continuously update components using `setInterval`. And I saw you applied answer which is based on `componentDidUpdate`, but it just won't work in my example. You can see it [here](http://codepen.io/cn007b/pen/Kaxvbj?editors=0010). – cn007b Feb 08 '17 at 06:15
  • @MykolaGolubyev Please accept that as the answer as well. – hazardous Feb 08 '17 at 10:57

11 Answers11

6

React already have an addon for these situations. ReactCSSTransitionGroup

ReactCSSTransitionGroup is a high-level API based on ReactTransitionGroup and is an easy way to perform CSS transitions and animations when a React component enters or leaves the DOM. It's inspired by the excellent ng-animate library.

You can easily animate items that are entering or leaving a specific parent.

var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

const nextId = (() => {
  let lastId = 0;
  return () => ++lastId;
})();

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {items: [
      {id: nextId(), text: 'hello'}, 
      {id: nextId(), text: 'world'}, 
      {id: nextId(), text: 'click'}, 
      {id: nextId(), text: 'me'}
    ]};
    this.handleAdd = this.handleAdd.bind(this);
  }

  handleAdd() {
    const newItems = this.state.items.concat([
      {id: nextId(), text: prompt('Enter some text')}
    ]);
    this.setState({items: newItems});
  }

  handleRemove(toRemove) {
    let newItems = this.state.items.filter(item => item.id !== toRemove.id);
    this.setState({items: newItems});
  }

  render() {
    const items = this.state.items.map((item) => (
      <div key={item.id} onClick={() => this.handleRemove(item)}>
        {item.text}
      </div>
    ));

    return (
      <div>
        <button className="add-todo" onClick={this.handleAdd}>Add Item</button>        
        <ReactCSSTransitionGroup
          transitionName="example"
          transitionEnterTimeout={500}
          transitionLeaveTimeout={300}>
          {items}
        </ReactCSSTransitionGroup>
      </div>
    );
  }
}

ReactDOM.render(<TodoList/>, document.getElementById("app"));
.example-enter {  
  background-color: #FFDCFF;
  color: white;
}

.example-enter.example-enter-active {
  background-color: #9E1E9E;  
  transition: background-color 0.5s ease;
}

.example-leave {
  background-color: #FFDCFF;
  color: white;
}

.example-leave.example-leave-active {
  background-color: #9E1E9E;  
  transition: background-color 0.3s ease;
}

.add-todo {
  margin-bottom: 10px;
}
<script src="https://unpkg.com/react@15/dist/react-with-addons.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>

<div id="app"></div>
Tiago Engel
  • 3,533
  • 1
  • 17
  • 22
  • have seen this before. I don't have a flat structure like TODO list. And items themselves can be changed. – Mykola Golubyev Feb 01 '17 at 23:58
  • For the not flat structure problem you will have to add the `ReactCSSTransitionGroup` component in every component to animate their children. As for the changes in the item itself, do you mean you want to give the user a feedback if any prop/state changes? – Tiago Engel Feb 02 '17 at 00:35
  • All those children represent a RichText, so if anything changes in it I want to provide a visual clue. I wouldn't want to wrap every Container like (Paragraph) element into a CSS Transition. – Mykola Golubyev Feb 02 '17 at 23:45
  • 2
    My take on this would be wrapping all childs with the CSS transition. You don't necessarily need to do it manually in every component, you can, for example, create a HOC that adds the transition and use it in all components you want. – Tiago Engel Feb 03 '17 at 11:53
  • I am using idx as a key for children as there is no natural key. Inserting a new element at the beginning will trigger updates on all the existing elements. – Mykola Golubyev Feb 04 '17 at 18:33
  • Yes, you can't have duplicate keys as react will use them internally for the transitions, in fact using the index as id can cause whole lot of other problems and is not recommended. You can use a function to generate unique ids like I did in the example. – Tiago Engel Feb 04 '17 at 20:48
4

Last edit

Ok now you finally included the data needed to understand it. You can handle it absolutely with componentDidMount, componentWillReceiveProps and componentDidUpdate, with some instance variables to keep some internal state unrelated to rendering in your "content" components.

Here you have a working snippet. I'm using some fake buttons to add new content to the end of the list and modify any of the items. This is a mock of your JSON messages coming in, but I hope you get the gist of it.

My styling is pretty basic but you could add some CSS transitions/keyframe animations to make the highlighting last only for a while instead of being fixed. That's however a CSS question not a React one. ;)

const Component = React.Component

class ContentItem extends Component {
  constructor(props){
    super(props)
    this.handleClick = this.handleClick.bind(this)
    //new by default
    this._isNew = true
    this._isUpdated = false
  }
  componentDidMount(){
    this._isNew = false
  }
  componentDidUpdate(prevProps){    
    this._isUpdated = false     
  }
  componentWillReceiveProps(nextProps){
    if(nextProps.content !== this.props.content){
      this._isUpdated = true
    }
  }
  handleClick(e){
    //hack to simulate a change in a specific content
    this.props.onChange(this.props.index)
  }
  render(){
    const { content, index } = this.props
    const newStyle = this._isNew ? 'new' : ''
    const updatedStyle = this._isUpdated ? 'updated': ''
         
    return (
      <p className={ [newStyle, updatedStyle].join(' ') }>
        { content }
        <button style={{ float: 'right' }} onClick={ this.handleClick}>Change me</button>
      </p>
     )
  }
}

class Document extends Component {
  constructor(props){
    super(props)
    this.state = {
      content: [
        { type: 'p', content: 'Foo' },
        { type: 'p', content: 'Bar' }
      ]
    }
    this.addContent = this.addContent.bind(this)
    this.changeItem = this.changeItem.bind(this)
  }
  addContent(){
    //mock new content being added
    const newContent = [ ...this.state.content, { type: 'p', content: `Foo (created at) ${new Date().toLocaleTimeString()}` }]
    this.setState({ content: newContent })
  }
  changeItem(index){
    //mock an item being updated
    const newContent = this.state.content.map((item, i) => {
      if(i === index){
        return { ...item, content: item.content + ' Changed at ' + new Date().toLocaleTimeString() }
      }
      else return item
    })
    this.setState({ content: newContent })
  }
  render(){
    return (
      <div>
        <h1>HEY YOU</h1>
        <div className='doc'>
          {
            this.state.content.map((item, i) => 
              <ContentItem key={ i } index={ i } { ...item } onChange={ this.changeItem } />)
          }
        </div>
        <button onClick={ this.addContent }>Add paragraph</button>
      </div>
    )    
  }
}

ReactDOM.render(<Document />, document.getElementById('app'));
.new {
  background: #f00
}
.updated {
  background: #ff0
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
CharlieBrown
  • 4,143
  • 23
  • 24
  • 1
    thank you for the very detailed answer. I updated my question. Hope it clarifies things a bit – Mykola Golubyev Feb 01 '17 at 12:56
  • Hi Mykola I just did update myself as well. Hope it helps. – CharlieBrown Feb 03 '17 at 18:00
  • Thanks for the update. Document is being sent as a whole every time. There are no timestamps. Server doesn't know what part has changed. Consider someone just uploaded a new version of a document. Let me add the format to the question. – Mykola Golubyev Feb 04 '17 at 15:30
  • I don't think the last point you make is a pure css question. You code leaves the 'updated' class on a paragraph until something else changes. However if you click 'Change me' twice on the same paragraph, the 'updated' class stays there between your clicks. I don't see how you can implement 'blinking' with this behavior. – jetpackpony Feb 05 '17 at 08:47
  • Hi @jetpackpony since the op didn't specify the kind of "blinking" I produced the simplest working example. It should be pretty easy to use a CSS animation that fades out the background after N seconds. If you change twice the same paragraph, then it's been updated twice, and it should be highlighted twice isn't it? Sorry I'm not following you :) – CharlieBrown Feb 05 '17 at 09:24
  • @CharlieBrown the OP specified exactly what kind of blinking: "I want to achieve what Chrome inspector is showing when you are looking at HTML tree. It blinks DOM changes". It is supposed to change background color (which your code does) and then fade back to transparent (which your code does not). As I've mentioned above, when you click "Change me" twice, from Chrome point of view the change in class property happens only after the first click (you can actually open Chrome inspector and see for yourself). If you think I'm wrong, I dare you to implement the blinking based on your code :-) – jetpackpony Feb 05 '17 at 10:01
  • @CharlieBrown you can see my answer for the way it is supposed to work ;-) – jetpackpony Feb 05 '17 at 10:02
  • Really appreciate the effort you are putting in the answer! I hope you have enjoyed the exercise as well. I do think in terms of React however in this case I feel like an easier solution should be possible if React exposed certain API. In my mind React model is finished with a document being rendered. Highlighting is a utility. Current approach while being a proper React feels like an overkill (even with HOC, and there are around 10 basic components). – Mykola Golubyev Feb 07 '17 at 02:24
  • You're welcome @MykolaGolubyev. I still think adding your own timestamps to the socket data is the way to go, and the problem would be a pure data-oriented one - detecting changes in an Array of content item and marking it. If data comes with no timestamp, add it on first response and then diff the following messages. That's another question perhaps ;) If you want to do it from the DOM POV, I'd explore using refs or finally the MutationObserver API on the document component. – CharlieBrown Feb 07 '17 at 17:37
  • My problem with "figuring out differences myself" is that React is already doing it, so why repeat. And as for timestamps, adding them is like adding timestamps to a Word document's paragraph in my opinion. – Mykola Golubyev Feb 07 '17 at 22:31
3

I think you should use componentDidUpdate

from the docs:

componentDidUpdate(prevProps, prevState) is invoked immediately after updating occurs. This method is not called for the initial render.

Use this as an opportunity to operate on the DOM when the component has been updated. This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).

You could compare which component did change and then set a decorator style in the state, to use in your page.

  • I think it will be called regardless of whether component actually updated or not. This will not help if a new element is added. – Mykola Golubyev Feb 01 '17 at 23:59
3

You can write a HOC which wraps your leaf components within a PureComponent. This wrapper will then render the wrapped component with a special style when it detects a change through componentDidUpdate. It uses an internal flag to break infinite loop from a componentDidUpdate + setState situation.

Here's a sample code -

import React, {PureComponent} from "react";

let freshKid = (Wrapped, freshKidStyle) => {
    return class FreshKid extends PureComponent{
        state = {"freshKid" : true},
        componentDidUpdate(){
            if (this.freshKid){
                return;
            }
            this.freshKid = true;
            setTimeout(()=>this.setState(
                    {"freshKid" : false}, 
                    ()=>this.freshKid = false
                ), 
                100
            );
        }
        render(){
            let {freshKid} = this.state,
            {style, ..rest} = this.props,
            style = freshKid ? Object.assign({}, style, freshKidStyle) : style;

            return <Wrapped style={style} {...rest} />;
        }
    }
}

You can use this to wrap a leaf component like so -

let WrappedParagraph = freshKid(Paragraph, {"color":"orangered"});

Or export all leaf components pre-wrapped.

Please note that the code is only an idea. Also, you should put some more checks in the timeout body to verify id the component has not been unmounted, before calling setState.

hazardous
  • 10,627
  • 2
  • 40
  • 52
  • Like the idea of wrapping. It will not help with new children being added to a Container like parent (e.g. Paragraph has Text children elements). – Mykola Golubyev Feb 02 '17 at 23:47
  • This should only be used to wrap leaf elements, in this case then you should wrap Text. I hope that's the effect you want? E.g. if some new text appears in the paragraph, only that part should highlight? – hazardous Feb 03 '17 at 05:53
3

I think you should use shouldComponentUpdate, as far as I know, only here you can detect exactly your case.

Here my example:

class Text extends React.Component {
    constructor(props) {
        super(props);
        this.state = {textVal: this.props.text, className: ''};
    }
    shouldComponentUpdate(nextProps, nextState) {
        // Previous state equals to new state - so we have update nothing.
        if (this.state.textVal === nextProps.text) {
            this.state.className = '';
            return false;
        }
        // Here we have new state, so it is exactly our case!!!
        this.state.textVal = nextProps.text;
        this.state.className = 'blink';
        return true;
    }
    render() {
        return (<i className={this.state.className}>{this.state.textVal}</i>);
    }
}

It is only component Text (I left css and other components behind the scene), I think this code is most interesting, but you can try my working version on codepen, also here example with jquery and updations in loop.

cn007b
  • 16,596
  • 7
  • 59
  • 74
  • Modify your example with this and no blink: `setInterval(function () { paragraphs[0] = {id: 1, header: 'Sport', text: Date.now()}; ReactDOM.render(, document.getElementById('root')); }, 2000);`. Can you make it happen? – prosti Feb 02 '17 at 13:03
  • @prosti The point of my answer not in animation, but in approach! I decided to use css animation, because it is very simple, but you can use js animation or jquery. Here link to updated example: http://codepen.io/cn007b/pen/qRoJwO?editors=0010 I've used jquery just with purpose to write less code... but again, I just try to explain my point of view... – cn007b Feb 02 '17 at 13:48
  • @prosti Thank you, man! It was nice suggestion from your side! – cn007b Feb 02 '17 at 14:00
3

Before component is rendered you'll have to check if component's props changed. If they did, you'll have to add a class to the component and then remove that class after rendering. Adding css transition to that class will achieve you a blinking effect like in Chrome dev tools.

To detect changes in properties you should use componentWillReceiveProps(nextProps) component hook:

componentWillReceiveProps() is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.

This hook doesn't fire on component mount so additionally you will need to set the initial "highlighted" state in the constructor.

To remove the class after rendering you'll need to reset the state back to "not highlighted" in a setTimeout call so it happens outside the call stack and after the component will render.

In the example below type something in the input to see the paragraph highlighted:

class Paragraph extends React.Component {
  constructor(props) {
    super(props);
    this.state = { highlighted: true };
    this.resetHighlight();
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.text !== this.props.text) {
      this.setState({ highlighted: true });
      this.resetHighlight();
    }
  }

  resetHighlight() {
    setTimeout(() => {
      this.setState({ highlighted: false });
    }, 0);
  }

  render() {
    let classes = `paragraph${(this.state.highlighted) ? ' highlighted' : ''}`;
    return (
      <div className={classes}>{this.props.text}</div>
    );

  }
}
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "type in me" };
  }
  handleInput(e) {
    this.setState({ text: e.target.value });
  }
  render() {
    return (
      <div className="App">
        <Paragraph text={this.state.text} />
        <input type="text" onChange={this.handleInput.bind(this)} value={this.state.text} />
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
.paragraph {
  background-color: transparent;
  transition: 1s;
}

.paragraph.highlighted {
  background-color: red;
  transition: 0s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
jetpackpony
  • 1,270
  • 1
  • 12
  • 22
2

You can attach pass a callback as a ref to the node, and it will be invoked with the DOM node each time the DOM node is created/re-created.

You can use a common callback to track the created nodes.

lorefnon
  • 12,875
  • 6
  • 61
  • 93
  • I have a lot of nodes created recursively from a data. It maybe a bit cumbersome to pass ref callback all the way down. But I like the suggestion. Thank you. – Mykola Golubyev Jan 28 '17 at 20:50
  • I tried the approach. It won't trigger my ref callback if I only changed a value of a component (my key won't change, so the DOM node remains the same) – Mykola Golubyev Jan 28 '17 at 21:08
  • Oh yeah, I assumed by "changing" of node you meant actually changing the node identity. As far as I know React does not provide (well-documented) high level hooks to intercept into the reconciliation mechanism. Alternative approach is to keep track of data changes at the component level and deduce beforehand if a component would change. There is also [MutationObserver](https://developer.mozilla.org/en/docs/Web/API/MutationObserver) but it can be cumbersome to deal with. – lorefnon Jan 29 '17 at 06:06
  • If you're considering `MutationObserver`, have a look at [Mutation Summary](https://github.com/rafaelw/mutation-summary): *a JavaScript library that makes observing changes to the DOM fast, easy and safe.* – Joel Purra Jan 31 '17 at 23:17
  • MutationObserver looks promising. Thank you. – Mykola Golubyev Feb 02 '17 at 00:01
2

Unfortunately, React doesn't provide a hook to listen state changes from outside component. You can use componentDidUpdate(prevProps, nextProps) to be notified of state changes of your component but you have to keep a reference of the previous generated DOM and compare it manually with the new DOM (using dom-compare, for example). I think that's you already do with your current solution.

I tried an alternative solution using MutationObserver and this technique to get the modified element position relative to document and display a bordered layer above the mutated element. It seems to work well, but I didn't check performances.

mutationObserver.js

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

const observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    if (mutation.addedNodes) {
      mutation.addedNodes.forEach(showMutationLayer);
    }
  });
});

const showMutationLayer = (node) => {
  const mutationLayer = document.createElement('div');
  mutationLayer.style.position = 'absolute';
  mutationLayer.style.border = '2px solid red';
  document.body.appendChild(mutationLayer);
  if (node.nodeType === Node.TEXT_NODE) {
    node = node.parentNode;
  } 
  if (node.nodeType !== Node.ELEMENT_NODE) {
    return;
  }
  const { top, left, width, height } = getCoords(node);
  mutationLayer.style.top = `${top}px`;
  mutationLayer.style.left = `${left}px`;
  mutationLayer.style.width = `${width}px`;
  mutationLayer.style.height = `${height}px`;
  setTimeout(() => {
    document.body.removeChild(mutationLayer);
  }, 500);
};

function getCoords(elem) { // crossbrowser version
    const box = elem.getBoundingClientRect();
    const body = document.body;
    const docEl = document.documentElement;
    const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
    const clientTop = docEl.clientTop || body.clientTop || 0;
    const clientLeft = docEl.clientLeft || body.clientLeft || 0;
    const top  = box.top +  scrollTop - clientTop;
    const left = box.left + scrollLeft - clientLeft;
    return { 
      top: Math.round(top), 
      left: Math.round(left), 
      width: box.width,
      height: box.height
    };
}

export default {
   init(container) {
     observer.observe(container, {
       attributes: true,
       childList: true,
       characterData: true,
       subtree: true
     });
   } 
}

main.js

import React from 'react';
import {render} from 'react-dom';
import App from './App.js';
import mutationObserver from './mutationObserver.js';

const appContainer = document.querySelector('#app');

// Observe mutations when you are in a special 'debug' mode
// for example
if (process.env.NODE_ENV === 'debug') {
   mutationObserver.init(appContainer);
}

render(<App />, appContainer);

The advantages of this technique is you don't have to modify each of your components code to watch changes. You also don't modify the components generated DOM (the layer is outside the #app element). It is easy to enable/disable this functionality to preserve your application performance.

See it in action in this fiddle (you can edit the layer style, adding CSS transition for a nicer layer)

Community
  • 1
  • 1
Freez
  • 7,208
  • 2
  • 19
  • 29
  • As far as i understand, your answer based on `componentDidUpdate`. But it won't work in case when you update component continuously using `setInterval`, You can see it [here](http://codepen.io/cn007b/pen/Kaxvbj?editors=0010), and [here](http://codepen.io/cn007b/pen/qRoJwO?editors=0010) my example how I think it should works. – cn007b Feb 08 '17 at 06:20
2

I had faced same issue on a web app recently. My requirement was chrome like change notifier. The only difference that I need that feature globally. Since that feature required on UI (not important for server rendering) using a observer saved my life.

I set "notify-change" css class for the components and/or elements which I want to track. My observer listens the changes and checks if changed dom and/or its parents has "notify-change" class. If condition is matched then It simply adds "in" class for "notify-change" marked element to start fade effect. And removes "in" class in specific timeframe

(function () {
    const observer = new MutationObserver(function (mutations) {
        mutations.forEach(function (m) {
            let parent = m.target && m.target.parentNode;
            while (parent) {
                if (parent.classList && parent.classList.contains('notify-change')) {
                    break;
                }
                parent = parent.parentNode;
            }
            if (!parent) return;
            parent.classList.add('in');
            setTimeout(function () {
                try {
                    parent.classList.remove('in');
                } catch (err) {
                }
            }, 300);
        });
    });
    observer.observe(document.body, {subtree: true, characterData: true, characterDataOldValue: true});
})();

// testing

function test(){
  Array.from(document.querySelectorAll(".notify-change"))
  .forEach(e=>
    e.innerHTML = ["lorem", "ipsum", "sit" , "amet"][Math.floor(Math.random() * 4)]
  );
}

setInterval(test, 1000);
test();
.notify-change {
  transition: background-color 300ms ease-out;
  background-color:transparent;
}

.notify-change.in{
  background-color: yellow !important;
}
<div>Lorem ipsum dolor sit amet, eu quod duis eius sit, duo commodo impetus an, vidisse cotidieque an pro. Usu dicat invidunt et. Qui eu <span class="notify-change">Ne</span> impetus electram. At enim sapientem ius, ubique labore copiosae sea eu, commodo persecuti instructior ad his. Mazim dicit iisque sit ea, vel te oblique delenit.

Quo at <span class="notify-change">Ne</span> saperet <span class="notify-change">Ne</span>, in mei fugit eruditi nonumes, errem clita volumus an sea. Elitr delicatissimi cu quo, et vivendum lobortis usu. An invenire voluptatum his, has <span class="notify-change">Ne</span> incorrupte ad. Sensibus maiestatis necessitatibus sit eu, tota veri sea eu. Mei inani ocurreret maluisset <span class="notify-change">Ne</span>, mea ex mentitum deleniti.

Quidam conclusionemque sed an. <span class="notify-change">Ne</span> omnes utinam salutatus ius, sea quem necessitatibus no, ad vis antiopam tractatos. Ius cetero gloriatur ex, id per nisl zril similique, est id iriure scripta. Ne quot assentior theophrastus eum, dicam soleat eu ius. <span class="notify-change">Ne</span> vix nullam fabellas apeirian, nec odio convenire ex, mea at hinc partem utamur. In cibo antiopam duo.

Stet <span class="notify-change">Ne</span> no mel. Id sea adipisci assueverit, <span class="notify-change">Ne</span> erant habemus sit ei, albucius consulatu quo id. Sit oporteat argumentum ea, eam pertinax constituto <span class="notify-change">Ne</span> cu, sed ad graecis posidonium. Eos in labores civibus, has ad wisi idque.

Sit dolore <span class="notify-change">Ne</span> ne, vis eu perpetua vituperata interpretaris. Per dicat efficiendi et, eius appetere ea ius. Lorem commune mea an, at est exerci senserit. Facer viderer vel in, etiam putent alienum vix ei. Eu vim congue putent constituto, ad sit agam <span class="notify-change">Ne</span> integre, his ei veritus tacimates.</div>
Tolgahan Albayrak
  • 3,118
  • 1
  • 25
  • 28
1

I know this answer is out of the scope of your question, but it is well-intentioned another approach to your problem.

You are probably creating the app of the medium or large scale base on what you wrote, and as I can guess in that case, you should consider Flux or Redux architecture.

With this architecture in mind, your controller components can subscribe to the Application Store update and based on that you can update your presentational components.

prosti
  • 42,291
  • 14
  • 186
  • 151
1

You could create a decorator function (or HOC if you prefer the term) that uses partial application to observe changes based on a provided observer function.

(Very) simple pen to demonstrate the concept: http://codepen.io/anon/pen/wgjJvO?editors=0110

The key parts of the pen:

// decorator/HOC that accepts a change observer function
// and then a component to wrap
function observeChanges(observer) {
  return function changeObserverFactory(WrappedComponent) {
    return class ChangeObserver extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          changed: false
        }
      }

      componentWillReceiveProps(nextProps) {
        if (typeof observer === 'function') {
          observer(this.props, nextProps) && this.setState({ changed: true })
        } else if (this.props !== nextProps) {
          this.setState({ changed: true })
        }
      }

      componentDidUpdate() {
        if (this.state.changed) {
          setTimeout(() => this.setState({ changed: false }), 300)
        }
      }

      render() {
        return <WrappedComponent {...this.props} changed={this.state.changed} />
      }
    }
  }
}

// a simple component for showing a paragraph
const Paragraph = ({ changed, text }) => (
  <p className={`${changed ? 'changed' : ''}`}>{text}</p>
)

// a decorated change observer version of the paragraph,
// with custom change observer function
const ChangingParagraph = observeChanges(
  (props, nextProps) => props.text !== nextProps.text
)(Paragraph)

This would allow each individual component to determine what constitutes a change for itself.

A few side notes:

  • you should avoid doing state updates in componentDidUpdate,
    componentWillUpdate, and shouldComponentUpdate.
    componentWillReceiveProps is the place for that.

    If you need to update state in response to a prop change, use componentWillReceiveProps()

  • Looking directly at the DOM to find the differences seems like a lot of unnecessary work when you already have your state as the source of truth and existing methods to compare current and next state built right into the component lifecycle.

shadymoses
  • 3,273
  • 1
  • 19
  • 21