RenderOnVisible

by Jason Miller

HTML

<script src="https://unpkg.com/preact"></script>
<script src="https://unpkg.com/preact@beta"></script>

SCSS

html, body {
	font: 16px/1.21 'Helvetica Neue',helvetica,arial,sans-serif;
	font-weight: 300;
}

.demo {
	margin: 25px;
	padding: 25px;
	background: #DDD;
}

.fallback {
	position: relative;
	padding: 20px;
	margin: 20px;
	height: 60px;
}

.widget {
	position: relative;
	padding: 20px;
	margin: 20px;
	background: burlywood;
	box-shadow: 0 1px 5px rgba(0,0,0,0.3);
	font-size: 50px;
	color: #555;
	text-align: center;

	&[mounted] {
		transition: background 1s ease;
		background: #FFF;
		
		&:after {
			content: 'MOUNTED';
			position: absolute;
			display: block;
			left: 0;
			width: 100%;
			bottom: 10px;
			color: #999;
			font-size: 12px;
			animation: bounce .5s cubic-bezier(.5,0,.1,1.5) forwards 1;
		}
	}
}

@keyframes bounce {
	from { transform: scale(0.01); }
}

Babel + JSX

const { h, Component, render } = preact; /** @jsx h */

let q = [];
function enqueue(fn, delay) {
	if (q.push(fn)==1) setTimeout(processQueue, delay);
}
function processQueue() {
	let p;
	while (p=q.pop()) p();
}

class RenderOnVisible extends Component {
	static defaultProps = {
		fallback: <div />,
		debounce: 50,
		visible: false
	};
	state = {
		visible: this.props.visible
	};
	queueCheck = () => {
		if (!this.queued) {
			this.queued = true;
			enqueue(this.check, this.props.debounce);
		}
	};
	check = () => {
		this.queued = false;
		if (this.state.visible) {
			this.unhook();
			return true;
		}
		let bbox = this.base.getBoundingClientRect(),
			viewportHeight = window.innerHeight || document.documentElement.offsetHeight;
		if (bbox.top<=viewportHeight && bbox.bottom>=0) {
			this.unhook();
			this.setState({ visible:true });
			return true;
		}
	};
	unhook() {
		removeEventListener('scroll', this.queueCheck);
		removeEventListener('resize', this.queueCheck);
	}
	componentDidMount() {
		if (!this.check()) {
			addEventListener('scroll', this.queueCheck);
			addEventListener('resize', this.queueCheck);
		}
	}
	componentWillUnmount() {
		this.unhook();
	}
	render({ fallback, children }, { visible }) {
		let child = children[0];
		if (typeof child==='function') return child(visible);
		return visible ? child : fallback;
	}
}



class DemoWidget extends Component {
	componentWillMount() {
		//console.log(this.props.id+' mounting');
	}
	componentDidMount() {
		setTimeout( () => this.setState({ mounted:true }), 10);
	}
	componentWillUnmount() {
		console.log(this.props.id+' UNmounting');
	}
	render({ id }, { mounted=false }) {
		return (
			<div class="widget" mounted={mounted}>
				{id}
			</div>
		);
	}
}

const Demo = () => {
	let kids = [];
	for (let i=1; i<=100; i++) {
		kids.push(
			i>90 ? (
				// example: manually specify initial visibility:
				<RenderOnVisible visible>
					<DemoWidget id={i} />
				</RenderOnVisible>
			) : i<=50 ? (
				// example:...