简体   繁体   中英

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 (ie 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" (ie do not have to have special code to notify the inverted scrolling handler that they have changed).

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.

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.

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 {
...
}

Maybe you can have a look to react-virtualized which has an exemple for reverse scroll list.

 export default class Example extends Component { constructor (props) { super(props) this.state = { list: [] } } componentDidMount () { this._interval = setInterval(::this._updateFeed, 500) } componentWillUnmount () { clearInterval(this._interval) } render () { const { list } = this.state return ( <div className={styles.VirtualScrollExample}> <VirtualScroll ref='VirtualScroll' className={styles.VirtualScroll} width={300} height={200} rowHeight={60} rowCount={list.length} rowRenderer={::this._rowRenderer} /> </div> ) } _updateFeed () { const list = [ ...this.state.list ] list.unshift( // Add new item here ) this.setState({ list }) // If you want to scroll to the top you can do it like this this.refs.VirtualScroll.scrollToRow(0) } _rowRenderer (index) { return ( // Your markup goes here ) } } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM