Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bubbling componentWillUpdate and componentDidUpdate

I know this is not idiomatic React, but I'm working on a React component (an inverse scrollview) which needs to get notified of downstream virtual DOM changes both before they are rendered and after they are rendered.

This is a little hard to explain so I made a JSFiddle with a starting point: https://jsfiddle.net/7hwtxdap/2/

My goal is to have the log look like:

Begin log

render

rendered small

rendered small

rendered small

before update

rendered big

after update

before update

rendered big

after update

before update

rendered big

after update

I'm aware that React sometimes batches DOM changes and that's fine (i.e. the log events could be before update; rendered big; rendered big; after update; ...) -- important part is that I am actually notified before and after the DOM changes.


I can manually approximate the behavior by specifying callbacks, as done here: https://jsfiddle.net/7hwtxdap/4/. However, this approach does not scale—I need, for example, to have events bubble from descendents rather than children; and would rather not have to add these kind of event handlers to every component I use.


Use case:

I'm working on making an "inverted scrolling component" for messages (where, when new elements are added or existing elements change size, the basis for scroll change is from the bottom of the content rather than the top ala every messaging app) which has dynamically modified children (similar to the <MyElement /> class but with variable height, data from ajax, etc). These are not getting data from a Flux-like state store but rather using a pub-sub data sync model (approximated pretty well with the setTimeout()).

To make inverse scrolling work, you need to do something like this:

anyChildrenOfComponentWillUpdate() {
  let m = this.refs.x;
  this.scrollBottom = m.scrollHeight - m.scrollTop - m.offsetHeight;
}
anyChildrenOfComponentDidUpdate() {
  let m = this.refs.x;
  m.scrollTop = m.scrollHeight - m.offsetHeight - this.scrollBottom;
}

Importantly to this design, I'd like to be able to wrap the element around other elements that are not "inverted scrolling aware" (i.e. do not have to have special code to notify the inverted scrolling handler that they have changed).

like image 948
Aaron Yodaiken Avatar asked Jun 17 '16 17:06

Aaron Yodaiken


People also ask

What is the difference between the componentDidUpdate method and the componentDidMount method?

componentDidMount() : invoked immediately after a component is mounted (inserted into the DOM tree) componentDidUpdate(prevProps, prevState, snapshot) : is invoked immediately after updating occurs. This method is not called for the initial render.

What is componentDidUpdate?

The componentDidUpdate() method allows us to execute the React code when the component is updated. All the network requests that are to be made when the props passed to the component changes are coded here.

What is componentWillUpdate?

ReactJS – componentWillUpdate() MethodThis method is used during the updating phase of the React lifecycle. This function is generally called before the component is updated or when the state or props passed to the component changes.

Is componentDidUpdate called after render?

ComponentDidUpdate() is invoked immediately after updating occurs and is not called for the initial render. The most common use-case for the componentDidUpdate () lifecycle method is updating the DOM in response to Prop or State changes.


3 Answers

React, by design, respects that a setState call on a component should trigger a re-rendering of that component. The most reliable way I know of to hook onto a component's life-cycle is on the component itself.

How about you pass down a notifier() callback as a prop and then call this function within the componentWillMount and componentDidUpdate lifecycle hook of the children components?

Here's a working js-fiddle with this idea. Let me know what comes up.

Edit 1

If you'd rather not pass callbacks back and forth through every single child component, then I'd say you've just come across a specific use case of Redux and Flux.

With Redux, a component can, through a reducer function, change the state of a store. Every component watching for a change in that store is then re-rendered. Flux uses the same idea.

So I'd suggest you use redux to handle state setting. To get started, you could go through these tutorial videos by redux's creator, Dan Abramov.

like image 167
Cent Avatar answered Sep 22 '22 10:09

Cent


It's hard to know what the best solution is to your problem without knowing why you need this functionality. However, I'll address this specific point:

I need, for example, to have events bubble from descendents rather than children; and would rather not have to add these kind of event handlers to every component I use.

I would suggest two changes to your solution for this. The first is to consider using context, but make sure to read the warnings before deciding to do so. This will allow you to define your callbacks in a single component and have them instantly available in all descendants.

The second change would be to move all your logging logic into a separate component, and have other components inherit it as needed. This component would look something like:

class LoggingComponent extends React.Component {
  componentWillUpdate() {
    if (this.context.onWillUpdate) {
        this.context.onWillUpdate();
    }
  }
  componentDidUpdate() {
    if (this.context.onDidUpdate) {
        this.context.onDidUpdate();
    }
  }
}

LoggingComponent.contextTypes = {
    onWillUpdate: React.PropTypes.func,
    onDidUpdate: React.PropTypes.func
};

This way, you can easily add this functionality to new components without duplicating the logic. I've updated your jsfiddle with a working demo.

like image 37
James Brierley Avatar answered Sep 22 '22 10:09

James Brierley


As James said, you can use context to define a callback function which you can pass down to all descendant components and if you don't want to extend your components you can use a decorator to wrap a Higher order component around them. Something like this:

In your container:

class Container extends React.Component {
    static childContextTypes =  {
        log: React.PropTypes.func.isRequired
    };

   getChildContext() {
      return {
            log: (msg) => {
                console.log(msg)
            }
        }
    }

  render() {
    console.log("Begin Log")
    return (
        <div>
          <MyElement />
          <MyElement />
          <MyElement />
        </div>
    );
  }
}

The decorator class:

export const Logger = ComposedComponent => class extends React.Component {
    static contextTypes = {
        log: React.PropTypes.func.isRequired
    };

    constructor(props) {
        super(props)
    }

    componentWillReceiveProps(nextProps) {
        this.context.log('will receive props');
    }

    componentWillUpdate() {
        this.context.log('before update');
    }

    componentDidUpdate() {
        this.context.log('after update');
    }

  render() {
      return <ComposedComponent {...this.props}/>
  }
} 

Then decorate the components you want

@Logger
class MyElement extends React.Component {
...
}
like image 34
Smilev Avatar answered Sep 21 '22 10:09

Smilev