Skip to content

upgrade: react 19 #363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 27 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,37 @@
"version": "1.3.5",
"author": "Luis Merino <[email protected]>",
"engines": {
"node": ">=10.18.1"
"node": ">=20"
},
"bugs": {
"url": "https://github.com/researchgate/react-intersection-observer/issues"
},
"dependencies": {},
"devDependencies": {
"@babel/core": "7.20.2",
"@babel/preset-typescript": "7.18.6",
"@researchgate/babel-preset": "2.0.14",
"@researchgate/spire-config": "7.0.0",
"@storybook/addon-actions": "6.5.13",
"@storybook/addon-knobs": "6.4.0",
"@storybook/addon-options": "5.3.21",
"@storybook/react": "6.5.13",
"@testing-library/react-hooks": "7.0.2",
"@types/jest": "27.5.2",
"@types/react": "17.0.52",
"@types/react-dom": "17.0.18",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"babel-loader": "8.3.0",
"intersection-observer": "0.12.2",
"npm-run-all": "4.1.5",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-test-renderer": "17.0.2",
"spire": "4.1.2",
"spire-plugin-semantic-release": "4.1.0",
"storybook-readme": "5.0.9",
"typescript": "4.0.5"
"@babel/core": "^7.23.9",
"@babel/preset-typescript": "^7.23.3",
"@researchgate/babel-preset": "^2.0.14",
"@researchgate/spire-config": "^7.0.0",
"@storybook/addon-actions": "^7.6.10",
"@storybook/addon-knobs": "^7.0.2",
"@storybook/react": "^7.6.10",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^29.5.12",
"@types/react": "^18.2.58",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"babel-loader": "^9.1.3",
"intersection-observer": "^0.12.2",
"npm-run-all": "^4.1.5",
"prop-types": "^15.8.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-test-renderer": "^19.0.0",
"spire": "^5",
"spire-plugin-semantic-release": "^5",
"storybook-readme": "^5",
"typescript": "^5"
},
"files": [
"lib",
Expand All @@ -57,8 +56,8 @@
"module": "lib/es/index.js",
"types": "typings/index.d.ts",
"peerDependencies": {
"react": "^16.3.2 || ^17.0.0",
"react-dom": "^16.3.2 || ^17.0.0"
"react": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"repository": {
"type": "git",
Expand Down
143 changes: 41 additions & 102 deletions src/IntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import React from 'react';
import { findDOMNode } from 'react-dom';
import { createObserver, observeElement, unobserveElement } from './observer';
import { createObserver } from './observer';
import {
shallowCompare,
isChildrenWithRef,
hasOwnProperty,
toString,
} from './utils';
import { ChangeHandler, Options, Instance, TargetNode } from './types';
import { ChangeHandler, Options, Instance } from './types';

const observerOptions = <const>['root', 'rootMargin', 'threshold'];
const observableProps = <const>['root', 'rootMargin', 'threshold', 'disabled'];
Expand Down Expand Up @@ -36,129 +33,71 @@ interface Props extends Options {
onChange: ChangeHandler;
}

export default class ReactIntersectionObserver
extends React.Component<Props, {}>
implements Instance {
export default class ReactIntersectionObserver extends React.Component<Props> implements Instance {
static displayName = 'IntersectionObserver';

private targetNode?: TargetNode;
private prevTargetNode?: TargetNode;
public target?: TargetNode;
private targetNode?: Element;
public observer?: IntersectionObserver;

handleChange = (event: IntersectionObserverEntry) => {
this.props.onChange(event, this.externalUnobserve);
handleChange = (event: IntersectionObserverEntry, unobserve: () => void) => {
this.props.onChange(event, unobserve);
};

handleNode = <T extends React.ReactInstance | null | undefined>(
target: T
) => {
const { children } = this.props;
/**
* Forward hijacked ref to user.
*/
if (isChildrenWithRef<T>(children)) {
const childenRef = children.ref;
if (typeof childenRef === 'function') {
childenRef(target);
} else if (childenRef && hasOwnProperty.call(childenRef, 'current')) {
/*
* The children ref.current is read-only, we aren't allowed to do this, so
* in future release it has to go away, and the ref shall be
* forwarded and assigned to a DOM node by the user.
*/
(childenRef as React.MutableRefObject<T>).current = target;
}
}

this.targetNode = undefined;
if (target) {
const targetNode = findDOMNode(target);
if (targetNode && targetNode.nodeType === 1) {
this.targetNode = targetNode as Element;
}
}
};

observe = () => {
if (this.props.children == null || this.props.disabled) {
return false;
}
if (!this.targetNode) {
throw new Error(
"ReactIntersectionObserver: Can't find DOM node in the provided children. Make sure to render at least one DOM node in the tree."
);
}
this.observer = createObserver(getOptions(this.props));
this.target = this.targetNode;
observeElement(this);

return true;
};

unobserve = (target: TargetNode) => {
unobserveElement(this, target);
};

externalUnobserve = () => {
if (this.targetNode) {
this.unobserve(this.targetNode);
componentDidMount() {
if (typeof window !== 'undefined') {
this.observe();
}
};

getSnapshotBeforeUpdate(prevProps: Props) {
this.prevTargetNode = this.targetNode;
}

componentDidUpdate(prevProps: Props) {
const relatedPropsChanged = observableProps.some(
(prop: typeof observableProps[number]) =>
shallowCompare(this.props[prop], prevProps[prop])
(prop) => shallowCompare(this.props[prop], prevProps[prop])
);

if (relatedPropsChanged) {
if (this.prevTargetNode) {
if (!prevProps.disabled) {
this.unobserve(this.prevTargetNode);
}
}
this.unobserve();
this.observe();
}
}

return relatedPropsChanged;
componentWillUnmount() {
this.unobserve();
}

componentDidUpdate(_: any, __: any, relatedPropsChanged: boolean) {
let targetNodeChanged = false;
// check if we didn't unobserve previously due to a prop change
if (!relatedPropsChanged) {
targetNodeChanged = this.prevTargetNode !== this.targetNode;
// check we have a previous node we want to unobserve
if (targetNodeChanged && this.prevTargetNode != null) {
this.unobserve(this.prevTargetNode);
}
}
observe() {
if (!this.targetNode || this.props.disabled) return;

if (relatedPropsChanged || targetNodeChanged) {
this.observe();
}
const options = getOptions(this.props);
this.observer = createObserver(options);
this.observer.observe(this.targetNode);
}

componentDidMount() {
this.observe();
unobserve() {
if (this.observer && this.targetNode) {
this.observer.unobserve(this.targetNode);
this.observer = undefined;
}
}

componentWillUnmount() {
if (this.targetNode) {
this.unobserve(this.targetNode);
handleNode = (node: Element | null) => {
if (node) {
this.targetNode = node;
} else {
this.targetNode = undefined;
}
}
};

render() {
const { children } = this.props;

return children != null
? React.cloneElement(React.Children.only(children), {
ref: this.handleNode,
})
: null;
if (!children) return null;

return React.cloneElement(React.Children.only(children), {
ref: this.handleNode,
});
}
}



export * from './types';
17 changes: 10 additions & 7 deletions src/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ export function getPooled(options: IntersectionObserverInit = {}) {
const root = options.root || null;
const rootMargin = parseRootMargin(options.rootMargin);
const threshold = Array.isArray(options.threshold)
? options.threshold
? [...options.threshold] // Create a mutable copy of the array
: [options.threshold != null ? options.threshold : 0];
const observers = observerElementsMap.keys();
let observer;
while ((observer = observers.next().value)) {
const unmatched =
root !== observer.root ||
rootMargin !== observer.rootMargin ||
shallowCompare(threshold, observer.thresholds);
shallowCompare(threshold, Array.from(observer.thresholds)); // Convert to mutable array

if (!unmatched) {
return observer;
Expand All @@ -34,9 +34,9 @@ export function findObserverElement(
const elements = observerElementsMap.get(observer);
if (elements) {
const values = elements.values();
let element: Instance;
let element: Instance | undefined;
while ((element = values.next().value)) {
if (element.target === entry.target) {
if (element?.target === entry.target) {
return element;
}
}
Expand All @@ -58,7 +58,7 @@ export function callback(
const element = findObserverElement(observer, entries[i]);
/* istanbul ignore next line */
if (element) {
element.handleChange(entries[i]);
element.handleChange(entries[i], () => unobserveElement(element, entries[i].target));
}
}
}
Expand All @@ -81,8 +81,11 @@ export function observeElement(element: Instance) {
if (element.observer && !observerElementsMap.has(element.observer)) {
observerElementsMap.set(element.observer, new Set<Instance>());
}
observerElementsMap.get(element.observer)?.add(element);
element.observer!.observe(element.target!);
const elementsSet = observerElementsMap.get(element.observer);
if (elementsSet && element.observer && element.target) {
elementsSet.add(element);
element.observer.observe(element.target);
}
}

export function unobserveElement(element: Instance, target: TargetNode) {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface Options {
}

export interface Instance {
handleChange: (event: IntersectionObserverEntry) => void;
handleChange: (event: IntersectionObserverEntry, unobserve: Unobserve) => void;
observer?: IntersectionObserver;
target?: TargetNode;
}
6 changes: 3 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export function shallowCompare(
export const { hasOwnProperty, toString } = Object.prototype;

export function isChildrenWithRef<T>(
children: unknown
): children is React.RefAttributes<T> {
return children && hasOwnProperty.call(children, 'ref');
children: React.ReactElement | null | undefined
): children is React.ReactElement & { ref: React.Ref<T> } {
return !!children && hasOwnProperty.call(children, 'ref');
}

export function thresholdCacheKey(threshold: Options['threshold']) {
Expand Down
10 changes: 6 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES5",
"target": "ES2018",
"moduleResolution": "node",
"module": "ES2015",
"module": "ES2020",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
Expand All @@ -14,7 +14,9 @@
"jsx": "react",
"rootDir": "src",
"declarationDir": "./typings",
"outDir": "./lib/es"
"outDir": "./lib/es",
"skipLibCheck": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
Loading