1 the introduction
IntersectionObserver can easily determine whether elements are visible or not. In the previous close reading “Render on Demand with React”, we introduced the method of the native API. This time, we just saw its react-intersection-Observer package version. Let’s take a look at the React encapsulation idea.
2 brief introduction
The react-intersection- Observer provides a Hook useInView to determine whether an element is in the viewable area.
import React from "react";
import { useInView } from "react-intersection-observer";
const Component = (a)= > {
const [ref, inView] = useInView();
return ( <div ref={ref}> <h2>{`Header inside viewport ${inView}.`}</h2> </div> ); }; Copy the code
Since determining whether an element is visible is DOM-based, the ref callback must be passed to the DOM element that represents the element’s contour. In the example above, we passed ref to the outermost DIV.
UseInView also supports the following parameters:
root
: Checks whether view-based elements are visible. The default is the entire browser viewport.rootMargin
: root margin, which can fix pixel judgment in advance or delay during detection.threshold
: Indicates the visible threshold. The value ranges from 0 to 1. 0 indicates that all visible is visible, and 1 indicates that all visible is visible.triggerOnce
: Indicates whether to trigger only once.
3 intensive reading
UseInView is a Hook that uses ref to store the last DOM instance. State is a Boolean that stores whether the inView element is visible:
export function useInView(
options: IntersectionOptions = {},
) :InViewHookResponse {
const ref = React.useRef<Element>()
const [state, setState] = React.useState<State>(initialState)
// The middle part.. return [setRef, state.inView, state.entry] } Copy the code
SetRef is called when the component ref is assigned. The node callback is the new DOM node, so unobserve(ref.current) unobserves the old node and observe(node) listens for the new node. Ref. Current = node
// Middle part 1
const setRef = React.useCallback(
(node) = > {
if (ref.current) {
unobserve(ref.current);
} if (node) { observe( node, (inView, intersection) => { setState({ inView, entry: intersection }); if (inView && options.triggerOnce) { // If it should only trigger once, unobserve the element after it's inView unobserve(node); } }, options ); } // Store a reference to the node, so we can unobserve it later ref.current = node; }, [options.threshold, options.root, options.rootMargin, options.triggerOnce] ); Copy the code
If ref does not exist, the inView state will be cleared. After all, if no listener exists, the inView value will only be set to default false:
// Middle part 2
useEffect((a)= > {
if(! ref.current && state ! == initialState && ! options.triggerOnce) { // If we don't have a ref, then reset the state (unless the hook is set to only `triggerOnce`)
// This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
setState(initialState); } }); Copy the code
The observies. ts file has three core functions: observe, unobserve, and onChange.
observe
: Listens to whether element is visible.unobserve
: Cancel listening.onChange
: handlingobserve
The change of the callback.
Observe observe first. The observerId must be generated as a unique identifier. This identifier is determined by getRootId, rootMargin, and Threshold.
For the same root listener, obtain the observerInstance created by New IntersectionObserver() and call obServerInstance.observe. Here, two map-observer_map and INSTANCE_MAP are stored. The former is to ensure the uniqueness of IntersectionObserver instances under the same root, and the latter stores component inView and callback information. Used in the onChange function:
export function observe(
element: Element,
callback: ObserverInstanceCallback,
options: IntersectionObserverInit = {} ) {
// IntersectionObserver needs a threshold to trigger, so set it to 0 if it's not defined. // Modify the options object, since it's used in the onChange handler. if(! options.threshold) options.threshold =0; const { root, rootMargin, threshold } = options; // Validate that the element is not being used in another <Observer /> invariant( ! INSTANCE_MAP.has(element), "react-intersection-observer: Trying to observe %s, but it's already being observed by another instance.\nMake sure the `ref` is only used by a single <Observer /> instance.\n\n%s" ); /* istanbul ignore if */ if(! element)return; // Create a unique ID for this observer instance, based on the root, root margin and threshold. // An observer with the same options can be reused, so lets use this fact let observerId: string = getRootId(root) + (rootMargin ? `${threshold.toString()}_${rootMargin}` : threshold.toString()); let observerInstance = OBSERVER_MAP.get(observerId); if(! observerInstance) { observerInstance = new IntersectionObserver(onChange, options); /* istanbul ignore else */ if (observerId) OBSERVER_MAP.set(observerId, observerInstance); } const instance: ObserverInstance = { callback, element, inView: false. observerId, observer: observerInstance, // Make sure we have the thresholds value. It's undefined on a browser like Chrome 51. thresholds: observerInstance.thresholds || (Array.isArray(threshold) ? threshold : [threshold]), }; INSTANCE_MAP.set(element, instance); observerInstance.observe(element); return instance; } Copy the code
As for onChange function, because multi-element monitoring is adopted, it needs to traverse the changes array and judge that intersectionRatio exceeds the threshold to determine that it is in inView state. INSTANCE_MAP is used to obtain the corresponding instance. Modify its inView state and execute callback.
This callback corresponds to the second callback of observe in useInView Hook:
function onChange(changes: IntersectionObserverEntry[]) {
changes.forEach((intersection) = > {
const { isIntersecting, intersectionRatio, target } = intersection;
const instance = INSTANCE_MAP.get(target);
// Firefox can report a negative intersectionRatio when scrolling. /* istanbul ignore else */ if (instance && intersectionRatio >= 0) { // If threshold is an array, check if any of them intersects. This just triggers the onChange event multiple times. let inView = instance.thresholds.some((threshold) = > { return instance.inView ? intersectionRatio > threshold : intersectionRatio >= threshold; }); if(isIntersecting ! = =undefined) { // If isIntersecting is defined, ensure that the element is actually intersecting. // Otherwise it reports a threshold of 0 inView = inView && isIntersecting; } instance.inView = inView; instance.callback(inView, intersection); } }); } Copy the code
Finally, there is the unobserve unlisten implementation. When useInView setRef fills a new Node, it calls unobserve to unlisten the old Node.
First use INSTANCE_MAP to find the instance and call observer.unobserve(element) to destroy the listener. Finally, destroy unnecessary INSTANCE_MAP and ROOT_IDS stores.
export function unobserve(element: Element | null) {
if(! element)return;
const instance = INSTANCE_MAP.get(element);
if (instance) {
const { observerId, observer } = instance; const { root } = observer; observer.unobserve(element); // Check if we are still observing any elements with the same threshold. let itemsLeft = false; // Check if we still have observers configured with the same root. let rootObserved = false; /* istanbul ignore else */ if (observerId) { INSTANCE_MAP.forEach((item, key) = > { if(key ! == element) { if (item.observerId === observerId) { itemsLeft = true; rootObserved = true; } if (item.observer.root === root) { rootObserved = true; } } }); } if(! rootObserved && root) ROOT_IDS.delete(root); if(observer && ! itemsLeft) { // No more elements to observe for threshold, disconnect observer observer.disconnect(); } // Remove reference to element INSTANCE_MAP.delete(element); } } Copy the code
From the perspective of its implementation, in order to ensure the correct recognition of the existence of child elements, it is necessary to ensure that ref can be continuously passed to the outermost DOM of the component. If transmission fracture occurs, the current component will be judged not to be in the view. For example:
const Component = (a)= > {
const [ref, inView] = useInView();
return <Child ref={ref} />;
}; const Child = ({ loading, ref }) => { if (loading) { // This step will be judged as inView: false return <Spin />; } return <div ref={ref}>Child</div>; }; Copy the code
If your code prevents rendering based on inView, the component cannot change its state after loading. To avoid this, either do not let the ref pass break, or judge inView to true if the ref object is not available.
4 summarizes
There are two main ideas in the react-class library:
- Convert native apis to framework-specific apis, such as Hooks and ref from the React family.
- Handle life cycle bound cases, such as when the DOM is updated first
unobserve
againobserve
.
After looking at the source code for the react-intersection-Observer, do you see any optimizability? Welcome to the discussion.
React-intersection – Observer source code · Issue #257 · dt-fe/weekly
If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.
Pay attention to the front end of intensive reading wechat public account
Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)