]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/components/intersection_observer_article.js
Eliminate re-renders for intersection_observer_article.js (#5036)
[mastodon.git] / app / javascript / mastodon / components / intersection_observer_article.js
1 import React from 'react';
2 import PropTypes from 'prop-types';
3 import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
4 import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
5 import { is } from 'immutable';
6
7 // Diff these props in the "rendered" state
8 const updateOnPropsForRendered = ['id', 'index', 'listLength'];
9 // Diff these props in the "unrendered" state
10 const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
11
12 export default class IntersectionObserverArticle extends React.Component {
13
14 static propTypes = {
15 intersectionObserverWrapper: PropTypes.object.isRequired,
16 id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
17 index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
18 listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
19 saveHeightKey: PropTypes.string,
20 cachedHeight: PropTypes.number,
21 onHeightChange: PropTypes.func,
22 children: PropTypes.node,
23 };
24
25 state = {
26 isHidden: false, // set to true in requestIdleCallback to trigger un-render
27 }
28
29 shouldComponentUpdate (nextProps, nextState) {
30 const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
31 const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
32 if (!!isUnrendered !== !!willBeUnrendered) {
33 // If we're going from rendered to unrendered (or vice versa) then update
34 return true;
35 }
36 // Otherwise, diff based on props
37 const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
38 return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
39 }
40
41 componentDidMount () {
42 const { intersectionObserverWrapper, id } = this.props;
43
44 intersectionObserverWrapper.observe(
45 id,
46 this.node,
47 this.handleIntersection
48 );
49
50 this.componentMounted = true;
51 }
52
53 componentWillUnmount () {
54 const { intersectionObserverWrapper, id } = this.props;
55 intersectionObserverWrapper.unobserve(id, this.node);
56
57 this.componentMounted = false;
58 }
59
60 handleIntersection = (entry) => {
61 const { onHeightChange, saveHeightKey, id } = this.props;
62
63 if (this.node && this.node.children.length !== 0) {
64 // save the height of the fully-rendered element
65 this.height = getRectFromEntry(entry).height;
66
67 if (onHeightChange && saveHeightKey) {
68 onHeightChange(saveHeightKey, id, this.height);
69 }
70 }
71
72 this.setState((prevState) => {
73 if (prevState.isIntersecting && !entry.isIntersecting) {
74 scheduleIdleTask(this.hideIfNotIntersecting);
75 }
76 return {
77 isIntersecting: entry.isIntersecting,
78 isHidden: false,
79 };
80 });
81 }
82
83 hideIfNotIntersecting = () => {
84 if (!this.componentMounted) {
85 return;
86 }
87
88 // When the browser gets a chance, test if we're still not intersecting,
89 // and if so, set our isHidden to true to trigger an unrender. The point of
90 // this is to save DOM nodes and avoid using up too much memory.
91 // See: https://github.com/tootsuite/mastodon/issues/2900
92 this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
93 }
94
95 handleRef = (node) => {
96 this.node = node;
97 }
98
99 render () {
100 const { children, id, index, listLength, cachedHeight } = this.props;
101 const { isIntersecting, isHidden } = this.state;
102
103 if (!isIntersecting && (isHidden || cachedHeight)) {
104 return (
105 <article
106 ref={this.handleRef}
107 aria-posinset={index}
108 aria-setsize={listLength}
109 style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
110 data-id={id}
111 tabIndex='0'
112 >
113 {children && React.cloneElement(children, { hidden: true })}
114 </article>
115 );
116 }
117
118 return (
119 <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
120 {children && React.cloneElement(children, { hidden: false })}
121 </article>
122 );
123 }
124
125 }
This page took 0.09515 seconds and 4 git commands to generate.