기존 오픈소스 LazyLoad
출처 : https://github.com/twobin/react-lazyload/blob/master/src/index.jsx
import { debounce, throttle, isFunction } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styled from 'styled-components';
import { on, off } from './utils/event';
import scrollParent from './utils/scrollParent';
const Root = styled.div`
&:empty {
display: none;
}
`;
const Placeholder = styled.div``;
const defaultBoundingClientRect = {
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
};
const LISTEN_FLAG = 'data-lazyload-listened';
const listeners = [];
let pending = [];
// try to handle passive events
let passiveEventSupported = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
passiveEventSupported = true;
},
});
window.addEventListener('test', null, opts);
} catch (e) {}
// if they are supported, setup the optional params
// IMPORTANT: FALSE doubles as the default CAPTURE value!
const passiveEvent = passiveEventSupported
? { capture: false, passive: true }
: false;
/**
* Check if `component` is visible in overflow container `parent`
* @param {node} component React component
* @param {node} parent component's scroll parent
* @return {bool}
*/
const checkOverflowVisible = function checkOverflowVisible(component, parent) {
const node = component.ref;
let parentTop;
let parentLeft;
let parentHeight;
let parentWidth;
try {
({
top: parentTop,
left: parentLeft,
height: parentHeight,
width: parentWidth,
} = parent.getBoundingClientRect());
} catch (e) {
({
top: parentTop,
left: parentLeft,
height: parentHeight,
width: parentWidth,
} = defaultBoundingClientRect);
}
const windowInnerHeight =
window.innerHeight || document.documentElement.clientHeight;
const windowInnerWidth =
window.innerWidth || document.documentElement.clientWidth;
// calculate top and height of the intersection of the element's scrollParent and viewport
const intersectionTop = Math.max(parentTop, 0); // intersection's top relative to viewport
const intersectionLeft = Math.max(parentLeft, 0); // intersection's left relative to viewport
const intersectionHeight =
Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop; // height
const intersectionWidth =
Math.min(windowInnerWidth, parentLeft + parentWidth) - intersectionLeft; // width
// check whether the element is visible in the intersection
let top;
let left;
let height;
let width;
try {
({ top, left, height, width } = node.getBoundingClientRect());
} catch (e) {
({ top, left, height, width } = defaultBoundingClientRect);
}
const offsetTop = top - intersectionTop; // element's top relative to intersection
const offsetLeft = left - intersectionLeft; // element's left relative to intersection
const offsets = Array.isArray(component.props.offset)
? component.props.offset
: [component.props.offset, component.props.offset]; // Be compatible with previous API
return (
offsetTop - offsets[0] <= intersectionHeight &&
offsetTop + height + offsets[1] >= 0 &&
offsetLeft - offsets[0] <= intersectionWidth &&
offsetLeft + width + offsets[1] >= 0
);
};
/**
* Check if `component` is visible in document
* @param {node} component React component
* @return {bool}
*/
const checkNormalVisible = function checkNormalVisible(component) {
const node = component.ref;
// If this element is hidden by css rules somehow, it's definitely invisible
if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false;
let top;
let elementHeight;
try {
({ top, height: elementHeight } = node.getBoundingClientRect());
} catch (e) {
({ top, height: elementHeight } = defaultBoundingClientRect);
}
const windowInnerHeight =
window.innerHeight || document.documentElement.clientHeight;
const offsets = Array.isArray(component.props.offset)
? component.props.offset
: [component.props.offset, component.props.offset]; // Be compatible with previous API
return (
top - offsets[0] <= windowInnerHeight &&
top + elementHeight + offsets[1] >= 0
);
};
/**
* Detect if element is visible in viewport, if so, set `visible` state to true.
* If `once` prop is provided true, remove component as listener after checkVisible
*
* @param {React} component React component that respond to scroll and resize
*/
const checkVisible = function checkVisible(component) {
const node = component.ref;
if (!(node instanceof HTMLElement)) {
return;
}
const parent = scrollParent(node);
const isOverflow =
component.props.overflow &&
parent !== node.ownerDocument &&
parent !== document &&
parent !== document.documentElement;
const visible = isOverflow
? checkOverflowVisible(component, parent)
: checkNormalVisible(component);
if (visible) {
// Avoid extra render if previously is visible
if (!component.visible) {
if (component.props.once) {
pending.push(component);
}
component.visible = true;
component.forceUpdate();
}
} else if (!(component.props.once && component.visible)) {
component.visible = false;
if (component.props.unmountIfInvisible) {
component.forceUpdate();
}
}
};
const purgePending = function purgePending() {
pending.forEach((component) => {
const index = listeners.indexOf(component);
if (index !== -1) {
listeners.splice(index, 1);
}
});
pending = [];
};
const lazyLoadHandler = () => {
for (let i = 0; i < listeners.length; ++i) {
const listener = listeners[i];
checkVisible(listener);
}
// Remove `once` component in listeners
purgePending();
};
/**
* Forces the component to display regardless of whether the element is visible in the viewport.
*/
const forceVisible = () => {
for (let i = 0; i < listeners.length; ++i) {
const listener = listeners[i];
listener.visible = true;
listener.forceUpdate();
}
// Remove `once` component in listeners
purgePending();
};
// Depending on component's props
let delayType;
let finalLazyLoadHandler = null;
const isString = (string) => typeof string === 'string';
class LazyLoad extends Component {
constructor(props) {
super(props);
this.visible = false;
this.setRef = this.setRef.bind(this);
}
componentDidMount() {
// It's unlikely to change delay type on the fly, this is mainly
// designed for tests
let scrollport = window;
const { scrollContainer } = this.props;
if (scrollContainer) {
if (isString(scrollContainer)) {
scrollport = scrollport.document.querySelector(scrollContainer);
}
}
const needResetFinalLazyLoadHandler =
(this.props.debounce !== undefined && delayType === 'throttle') ||
(delayType === 'debounce' && this.props.debounce === undefined);
if (needResetFinalLazyLoadHandler) {
off(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
off(window, 'resize', finalLazyLoadHandler, passiveEvent);
finalLazyLoadHandler = null;
}
if (!finalLazyLoadHandler) {
if (this.props.debounce !== undefined) {
finalLazyLoadHandler = debounce(
lazyLoadHandler,
typeof this.props.debounce === 'number' ? this.props.debounce : 300,
);
delayType = 'debounce';
} else if (this.props.throttle !== undefined) {
finalLazyLoadHandler = throttle(
lazyLoadHandler,
typeof this.props.throttle === 'number' ? this.props.throttle : 300,
);
delayType = 'throttle';
} else {
finalLazyLoadHandler = lazyLoadHandler;
}
}
if (this.props.overflow) {
const parent = scrollParent(this.ref);
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = 1 + +parent.getAttribute(LISTEN_FLAG);
if (listenerCount === 1) {
parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent);
}
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
} else if (listeners.length === 0 || needResetFinalLazyLoadHandler) {
const { scroll, resize } = this.props;
if (scroll) {
on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
}
if (resize) {
on(window, 'resize', finalLazyLoadHandler, passiveEvent);
}
}
listeners.push(this);
checkVisible(this);
}
shouldComponentUpdate() {
return this.visible;
}
componentWillUnmount() {
if (this.props.overflow) {
const parent = scrollParent(this.ref);
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = +parent.getAttribute(LISTEN_FLAG) - 1;
if (listenerCount === 0) {
parent.removeEventListener(
'scroll',
finalLazyLoadHandler,
passiveEvent,
);
parent.removeAttribute(LISTEN_FLAG);
} else {
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
}
}
const index = listeners.indexOf(this);
if (index !== -1) {
listeners.splice(index, 1);
}
if (listeners.length === 0 && typeof window !== 'undefined') {
off(window, 'resize', finalLazyLoadHandler, passiveEvent);
off(window, 'scroll', finalLazyLoadHandler, passiveEvent);
}
}
setRef(element) {
if (element) {
this.ref = element;
}
}
render() {
const {
height,
children,
placeholder,
className,
style,
} = this.props;
return (
<Root className={className} ref={this.setRef} style={style}>
{
isFunction(children)
? children(this.visible)
: (
this.visible
? children
: (
placeholder || (
<Placeholder
style={{ height }}
/>
)
)
)
}
</Root>
);
}
}
LazyLoad.propTypes = {
className: PropTypes.string,
classNamePrefix: PropTypes.string,
once: PropTypes.bool,
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
offset: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),
overflow: PropTypes.bool,
resize: PropTypes.bool,
scroll: PropTypes.bool,
children: [
PropTypes.node,
PropTypes.func,
],
throttle: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
debounce: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
placeholder: PropTypes.node,
scrollContainer: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
unmountIfInvisible: PropTypes.bool,
style: PropTypes.object,
};
LazyLoad.defaultProps = {
className: '',
classNamePrefix: 'lazyload',
once: false,
offset: 0,
overflow: false,
resize: false,
scroll: true,
unmountIfInvisible: false,
};
export default LazyLoad;
export { lazyLoadHandler as forceCheck };
export { forceVisible };
위 코드를 리팩토링 하는 작업을 했다
기존 소스는 class형 컴포넌트로 구성되어 있고, 클래스변수를 이용한 visible로 해당 컴포넌트에 접근하는 방식을 취한다.
functional 컴포넌트에선 위 클래스변수를 대체하기 위해 forwardRef hoc를 적용하고 추가로 다른 방법을 적용해야만 하며,
찾아낸 방법들은
1. useImperativeHandle hook
2. visibleRef prop
이 있다
1.을 적용시 컴포넌트 내부로 함수들을 갖고 들어올 수 있으며 prop을 사용하는 함수들이 컴포넌트 내부의 범위에서 prop을 사용하며 리팩토링이 가능하다.
다만 usage에 있어 함수에 접근하는 방법, visible에 접근하는 방법이 다르다
1.을 적용한 코드는 다음과 같다
import { debounce, throttle, isFunction } from 'lodash';
import PropTypes from 'prop-types';
import React, { useState, useRef, useEffect, useCallback, useImperativeHandle } from 'react';
import styled from 'styled-components';
import withForwardedRef from 'hocs/withForwardedRef';
import { on, off } from './utils/event';
import scrollParent from './utils/scrollParent';
const propTypes = {
className: PropTypes.string,
classNamePrefix: PropTypes.string,
forwardedRef: PropTypes.object.isRequired,
once: PropTypes.bool,
height: PropTypes.oneOfType([
PropTypes.number, PropTypes.string
]),
offset: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),
overflow: PropTypes.bool,
resize: PropTypes.bool,
scroll: PropTypes.bool,
children: [
PropTypes.node,
PropTypes.func,
],
throttle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.bool
]),
debounce: PropTypes.oneOfType([
PropTypes.number,
PropTypes.bool
]),
placeholder: PropTypes.node,
scrollContainer: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
unmountIfInvisible: PropTypes.bool,
style: PropTypes.object,
};
const defaultProps = {
className: '',
classNamePrefix: 'lazyload',
forwardedRef: undefined,
once: false,
height: undefined,
offset: 0,
overflow: false,
resize: false,
scroll: true,
children: undefined,
throttle: undefined,
debounce: undefined,
placeholder: undefined,
scrollContainer: undefined,
unmountIfInvisible: false,
style: undefined,
};
// ====
const Root = styled.div`
&:empty {
display: none;
}
`;
const Placeholder = styled.div``;
const LISTEN_FLAG = 'data-lazyload-listened';
const listeners = [];
const pending = [];
/**
* check whether `passive` is supported or not
*/
const checkIsPassive = () => {
// try to handle passive events
let passiveEventSupported = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
passiveEventSupported = true;
},
});
window.addEventListener('test', null, opts);
} catch (e) {}
return (passiveEventSupported
? { capture: false, passive: true }
: false);
}
// if they are supported, setup the optional params
// IMPORTANT: FALSE doubles as the default CAPTURE value!
const passiveEvent = checkIsPassive();
const isString = (string) => typeof string === 'string';
const LazyLoad = ({
forwardedRef,
...props
}) => {
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const visibleRef = useRef(false);
const rootRef = useRef(null);
const delayTypeRef = useRef(null);
const finalLazyLoadHandlerRef = useRef(null);
const {
height,
children,
placeholder,
className,
style,
} = props;
const purgePending = () => {
pending.forEach((component) => {
const index = listeners.indexOf(component);
if (index !== -1) {
listeners.splice(index, 1);
}
});
pending = [];
};
const lazyLoadHandler = () => {
listeners.forEach((listener) => {
checkVisible(listener);
})
// Remove `once` component in listeners
purgePending();
};
/**
* Forces the component to display regardless of whether the element is visible in the viewport.
*/
const forceVisible = () => {
listeners.forEach((listener) => {
visibleRef.current = true;
listener.forceUpdate();
})
// Remove `once` component in listeners
purgePending();
};
/**
* Check if `component` is visible in overflow container `parent`
* @param {node} component React component
* @param {node} parent component's scroll parent
* @return {bool}
*/
const checkOverflowVisible = (componentRootRef, parent) => {
const node = componentRootRef.current;
const {
top: parentTop = 0,
left: parentLeft = 0,
height: parentHeight = 0,
width: parentWidth = 0,
} = parent.getBoundingClientRect();
const windowInnerHeight =
window.innerHeight || document.documentElement.clientHeight;
const windowInnerWidth =
window.innerWidth || document.documentElement.clientWidth;
// calculate top and height of the intersection of the element's scrollParent and viewport
const intersectionTop = parentTop > 0 ? parentTop : 0; // intersection's top relative to viewport
const intersectionLeft = parentLeft > 0 ? parentLeft : 0; // intersection's left relative to viewport
const intersectionHeight = ((parentTop + parentHeight >= windowInnerHeight)
? windowInnerHeight
: (parentTop + parentHeight))
- intersectionTop;
const intersectionWidth = ((parentLeft + parentWidth >= windowInnerWidth)
? windowInnerWidth
: (parentLeft + parentWidth))
- intersectionLeft;
// check whether the element is visible in the intersection
const { top = 0, left = 0, height = 0, width = 0 } = node.getBoundingClientRect();
const offsetTop = top - intersectionTop; // element's top relative to intersection
const offsetLeft = left - intersectionLeft; // element's left relative to intersection
const offsets = Array.isArray(props.offset)
? props.offset
: [props.offset, props.offset]; // Be compatible with previous API
return (
offsetTop - offsets[0] <= intersectionHeight &&
offsetTop + height + offsets[1] >= 0 &&
offsetLeft - offsets[0] <= intersectionWidth &&
offsetLeft + width + offsets[1] >= 0
);
};
/**
* Check if `component` is visible in document
* @param {node} componentRootRef React component
* @return {bool}
*/
const checkNormalVisible = (componentRootRef) => {
const node = componentRootRef.current;
// If this element is hidden by css rules somehow, it's definitely invisible
if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false;
const { top = 0, height: elementHeight = 0, } = node.getBoundingClientRect();
const windowInnerHeight =
window.innerHeight || document.documentElement.clientHeight;
const offsets = Array.isArray(props.offset)
? props.offset
: [props.offset, props.offset]; // Be compatible with previous API
// offsets = [100,100] 까지 고려했을 때 해당 뷰의 top이 현재 화면의 하단보다 위에 있고(좌표값이 더 작고) &&
// 뷰의 bottom부분이 화면의 상단보다 아래에 있으면(이미 화면에서 지나간 경우 처리) => true
return (
top - offsets[0] <= windowInnerHeight &&
top + elementHeight + offsets[1] >= 0
);
};
const checkVisible = (componentRootRef) => {
const node = componentRootRef.current;
if (!(node instanceof HTMLElement)) {
return;
}
const parent = scrollParent(node);
const isOverflow =
props.overflow &&
parent !== node.ownerDocument &&
parent !== document &&
parent !== document.documentElement;
const visible = isOverflow
? checkOverflowVisible(componentRootRef, parent)
: checkNormalVisible(componentRootRef);
if (visible) {
// Avoid extra render if previously is visible
if (!visibleRef.current) {
if (props.once) {
pending.push(componentRootRef);
}
visibleRef.current = true;
forceUpdate();
}
} else if (!(props.once && visibleRef.current)) {
visibleRef.current = false;
if (props.unmountIfInvisible) {
forceUpdate();
}
}
};
useImperativeHandle(forwardedRef, () => ({
forceVisible: () => forceVisible,
lazyLoadHandlerRef: () => lazyLoadHandler,
visibleRef,
}));
useEffect(() => {
if (visibleRef.current) forceUpdate();
}, [visibleRef.current]);
useEffect(() => {
const { scrollContainer } = props;
const parent = scrollParent(rootRef.current);
const scrollport = scrollContainer
? isString(scrollContainer)
? scrollport.document.querySelector(scrollContainer)
: window
: window;
if ((props.debounce !== undefined && delayTypeRef.current === 'throttle') ||
(delayTypeRef.current === 'debounce' && props.debounce === undefined)) {
off(scrollport, 'scroll', finalLazyLoadHandlerRef.current, passiveEvent);
off(window, 'resize', finalLazyLoadHandlerRef.current, passiveEvent);
finalLazyLoadHandlerRef.current = null;
}
if (!finalLazyLoadHandlerRef.current) {
delayTypeRef.current = (props.debounce !== undefined)
? 'debounce'
: (props.throttle !== undefined)
? 'throttle'
: undefined;
if (delayTypeRef.current === 'debounce') {
finalLazyLoadHandlerRef.current = debounce(
lazyLoadHandler,
typeof props.debounce === 'number' ? props.debounce : 300,
);
} else if (delayTypeRef.current === 'throttle') {
finalLazyLoadHandlerRef.current = throttle(
lazyLoadHandler,
typeof props.throttle === 'number' ? props.throttle : 300,
);
} else {
finalLazyLoadHandlerRef.current = () => lazyLoadHandler;
}
}
if (props.overflow) {
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = 1 + +parent.getAttribute(LISTEN_FLAG);
if (listenerCount === 1) {
parent.addEventListener('scroll', finalLazyLoadHandlerRef.current, passiveEvent);
}
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
} else if (listeners.length === 0 || needResetFinalLazyLoadHandler) {
const { scroll, resize } = props;
if (scroll) {
on(scrollport, 'scroll', finalLazyLoadHandlerRef.current, passiveEvent);
}
if (resize) {
on(window, 'resize', finalLazyLoadHandlerRef.current, passiveEvent);
}
}
listeners.push(rootRef);
checkVisible(rootRef);
return () => {
if (props.overflow) {
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = +parent.getAttribute(LISTEN_FLAG) - 1;
if (listenerCount === 0) {
parent.removeEventListener(
'scroll',
finalLazyLoadHandlerRef.current,
passiveEvent,
);
parent.removeAttribute(LISTEN_FLAG);
} else {
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
}
}
const index = listeners.indexOf(rootRef);
if (index !== -1) {
listeners.splice(index, 1);
}
if (listeners.length === 0 && typeof window !== 'undefined') {
off(window, 'resize', finalLazyLoadHandlerRef.current, passiveEvent);
off(window, 'scroll', finalLazyLoadHandlerRef.current, passiveEvent);
}
}
}, [])
return (
<Root className={className} ref={rootRef} style={style}>
{
isFunction(children)
? children(visibleRef.current)
: (
visibleRef.current
? children
: (
placeholder || (
<Placeholder
style={{ height }}
/>
)
)
)
}
</Root>
);
}
LazyLoad.propTypes = propTypes;
LazyLoad.defaultProps = defaultProps;
export default withForwardedRef(LazyLoad);
*useImperativeHandle을 사용하며 visibleRef 적용이 안되는 이유는, 매개변수로 visible을 넘겨받을 수가 없어지기 때문이다.
import { debounce, throttle, isFunction } from 'lodash';
import PropTypes from 'prop-types';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import withForwardedRef from 'hocs/withForwardedRef';
import { on, off } from './utils/event';
import scrollParent from './utils/scrollParent';
const propTypes = {
className: PropTypes.string,
classNamePrefix: PropTypes.string,
forwardedRef: PropTypes.object.isRequired,
once: PropTypes.bool,
height: PropTypes.oneOfType([
PropTypes.number, PropTypes.string
]),
offset: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),
overflow: PropTypes.bool,
resize: PropTypes.bool,
scroll: PropTypes.bool,
children: [
PropTypes.node,
PropTypes.func,
],
throttle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.bool
]),
debounce: PropTypes.oneOfType([
PropTypes.number,
PropTypes.bool
]),
placeholder: PropTypes.node,
scrollContainer: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
unmountIfInvisible: PropTypes.bool,
style: PropTypes.object,
};
const defaultProps = {
className: '',
classNamePrefix: 'lazyload',
forwardedRef: undefined,
once: false,
height: undefined,
offset: 0,
overflow: false,
resize: false,
scroll: true,
children: undefined,
throttle: undefined,
debounce: undefined,
placeholder: undefined,
scrollContainer: undefined,
unmountIfInvisible: false,
style: undefined,
};
// ====
const Root = styled.div`
&:empty {
display: none;
}
`;
const Placeholder = styled.div``;
const LISTEN_FLAG = 'data-lazyload-listened';
const listeners = [];
let pending = [];
//check whether `passive` is supported or not
const checkIsPassive = () => {
// try to handle passive events
let passiveEventSupported = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
passiveEventSupported = true;
},
});
window.addEventListener('passiveEventTest', null, opts);
} catch (e) {}
return (
passiveEventSupported
? { capture: false, passive: true }
: false
);
};
/**
* if they are supported, setup the optional params
* IMPORTANT: FALSE doubles as the default CAPTURE value!
*/
const passiveEvent = checkIsPassive();
const isString = (string) => typeof string === 'string';
const purgePending = () => {
pending.forEach((component) => {
const index = listeners.indexOf(component);
if (index !== -1) {
listeners.splice(index, 1);
}
});
pending = [];
};
const lazyLoadHandler = (
props,
forceUpdate,
visibleRef,
) => {
listeners.forEach((listener) => {
checkVisible(listener, props, forceUpdate, visibleRef);
});
// Remove `once` component in listeners
purgePending();
};
const checkVisible = (
componentEle,
props,
forceUpdate,
visibleRef,
) => {
const node = componentEle.current;
if (!(node instanceof HTMLElement)) {
return;
}
const parentEle = scrollParent(node);
const isOverflow = (
props.overflow
&& (parentEle !== node.ownerDocument)
&& (parentEle !== document)
&& (parentEle !== document.documentElement)
);
const visible = isOverflow
? checkOverflowVisible(componentEle, parentEle, props)
: checkNormalVisible(componentEle, props);
if (visible) {
// Avoid extra render if previously is visible
if (!visibleRef.current) {
if (props.once) {
pending.push(componentEle);
}
visibleRef.current = true;
forceUpdate();
}
} else if (!(props.once && visibleRef.current)) {
visibleRef.current = false;
if (props.unmountIfInvisible) {
forceUpdate();
}
}
};
/**
* Check if `componentEle` is visible in overflow container `parent`
* @param {node} componentEle React component
* @param {node} parentEle component's scroll parent
* @return {bool}
*/
const checkOverflowVisible = (
componentEle,
parentEle,
props,
) => {
const node = componentEle.current;
const {
top: parentTop = 0,
left: parentLeft = 0,
height: parentHeight = 0,
width: parentWidth = 0,
} = parentEle.getBoundingClientRect();
const windowInnerHeight = (
window.innerHeight
|| document.documentElement.clientHeight
);
const windowInnerWidth = (
window.innerWidth
|| document.documentElement.clientWidth
);
// calculate top and height of the intersection of the element's scrollParent and viewport
// intersection's top relative to viewport
const intersectionTop = parentTop > 0 ? parentTop : 0;
// intersection's left relative to viewport
const intersectionLeft = parentLeft > 0 ? parentLeft : 0;
const intersectionHeight = (
(parentTop + parentHeight >= windowInnerHeight)
? windowInnerHeight
: (parentTop + parentHeight)
)
- intersectionTop;
const intersectionWidth = (
(parentLeft + parentWidth >= windowInnerWidth)
? windowInnerWidth
: (parentLeft + parentWidth)
)
- intersectionLeft;
// check whether the element is visible in the intersection
const {
top = 0,
left = 0,
height = 0,
width = 0,
} = node.getBoundingClientRect();
// element's top relative to intersection
const offsetTop = top - intersectionTop;
// element's left relative to intersection
const offsetLeft = left - intersectionLeft;
// Be compatible with previous API
const [leftEdgeOffset, topEdgeOffset] = (
Array.isArray(props.offset)
? props.offset
: [props.offset, props.offset]
);
return (
offsetTop - leftEdgeOffset <= intersectionHeight &&
offsetTop + height + topEdgeOffset >= 0 &&
offsetLeft - leftEdgeOffset <= intersectionWidth &&
offsetLeft + width + topEdgeOffset >= 0
);
};
/**
* Check if `component` is visible in document
* @param {node} componentEle React component
* @return {bool}
*/
const checkNormalVisible = (
componentEle,
props
) => {
const node = componentEle.current;
// If this element is hidden by css rules somehow, it's definitely invisible
if (
!(node.offsetWidth
|| node.offsetHeight
|| node.getClientRects().length)
) return false;
const { top = 0, height: elementHeight = 0, } = node.getBoundingClientRect();
const windowInnerHeight = (
window.innerHeight
|| document.documentElement.clientHeight
);
// Be compatible with previous API
const [leftEdgeOffset, topEdgeOffset] = (
Array.isArray(props.offset)
? props.offset
: [props.offset, props.offset]
);
/**
* offsets = [100,100] as a default value, 고려했을 때 해당 뷰의 top이 현재 화면의 하단보다 위에 있고(좌표값이 더 작고) &&
* 뷰의 bottom부분이 화면의 상단보다 아래에 있으면(이미 화면에서 지나간 경우 처리) => true
*/
return (
top - leftEdgeOffset <= windowInnerHeight &&
top + elementHeight + topEdgeOffset >= 0
);
};
// Forces the component to display regardless of whether the element is visible in the viewport.
const forceVisible = (visibleRef) => {
listeners.forEach((listener) => {
visibleRef.current = true;
listener.forceUpdate();
});
// Remove `once` component in listeners
purgePending();
};
const BaseLazyLoad = ({
visibleRef,
forwardedRef: rootRef,
...props
}) => {
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const delayTypeRef = useRef(null);
const finalLazyLoadHandlerRef = useRef(null);
const {
height,
children,
placeholder,
className,
style,
} = props;
useEffect(() => {
const { scrollContainer } = props;
const parentEle = scrollParent(rootRef.current);
const scrollport = (
scrollContainer
? isString(scrollContainer)
? scrollport.document.querySelector(scrollContainer)
: window
: window
);
const needResetFinalLazyLoadHandler =(
(props.debounce !== undefined && delayTypeRef.current === 'throttle')
|| (delayTypeRef.current === 'debounce' && props.debounce === undefined)
);
if (needResetFinalLazyLoadHandler) {
off(scrollport, 'scroll', finalLazyLoadHandlerRef.current, passiveEvent);
off(window, 'resize', finalLazyLoadHandlerRef.current, passiveEvent);
finalLazyLoadHandlerRef.current = null;
}
if (!finalLazyLoadHandlerRef.current) {
delayTypeRef.current = (
(props.debounce !== undefined)
? 'debounce'
: (props.throttle !== undefined)
? 'throttle'
: undefined
);
if (delayTypeRef.current === 'debounce') {
finalLazyLoadHandlerRef.current = debounce(
() => lazyLoadHandler(rootRef, props, forceUpdate, visibleRef),
typeof props.debounce === 'number' ? props.debounce : 300,
);
} else if (delayTypeRef.current === 'throttle') {
finalLazyLoadHandlerRef.current = throttle(
() => lazyLoadHandler(rootRef, props, forceUpdate, visibleRef),
typeof props.throttle === 'number' ? props.throttle : 300,
);
} else {
finalLazyLoadHandlerRef.current = () => (
lazyLoadHandler(
rootRef,
props,
forceUpdate,
visibleRef,
)
);
}
}
if (props.overflow) {
if (parentEle && typeof parentEle.getAttribute === 'function') {
const listenerCount = 1 + +parentEle.getAttribute(LISTEN_FLAG);
if (listenerCount === 1) {
parentEle.addEventListener(
'scroll',
finalLazyLoadHandlerRef.current,
passiveEvent,
);
}
parentEle.setAttribute(LISTEN_FLAG, listenerCount);
}
} else if (listeners.length === 0 || needResetFinalLazyLoadHandler) {
const { scroll, resize } = props;
if (scroll) {
on(scrollport, 'scroll', finalLazyLoadHandlerRef.current, passiveEvent);
}
if (resize) {
on(window, 'resize', finalLazyLoadHandlerRef.current, passiveEvent);
}
}
listeners.push(rootRef);
checkVisible(rootRef, props, forceUpdate, visibleRef);
return () => {
if (props.overflow) {
if (parentEle && typeof parentEle.getAttribute === 'function') {
const listenerCount = +parentEle.getAttribute(LISTEN_FLAG) - 1;
if (listenerCount === 0) {
parentEle.removeEventListener(
'scroll',
finalLazyLoadHandlerRef.current,
passiveEvent,
);
parentEle.removeAttribute(LISTEN_FLAG);
} else {
parentEle.setAttribute(LISTEN_FLAG, listenerCount);
}
}
}
const index = listeners.indexOf(rootRef);
if (index !== -1) {
listeners.splice(index, 1);
}
if (listeners.length === 0 && typeof window !== 'undefined') {
off(window, 'resize', finalLazyLoadHandlerRef.current, passiveEvent);
off(window, 'scroll', finalLazyLoadHandlerRef.current, passiveEvent);
}
}
}, [])
return (
<Root className={className} ref={rootRef} style={style}>
{
isFunction(children)
? children(visibleRef.current)
: (
visibleRef.current
? children
: (
placeholder || (
<Placeholder
style={{ height }}
/>
)
)
)
}
</Root>
);
};
BaseLazyLoad.propTypes = propTypes;
BaseLazyLoad.defaultProps = defaultProps;
export default withForwardedRef(BaseLazyLoad);
export { lazyLoadHandler as forceCheck };
export { forceVisible };