Skip to content

Commit

Permalink
fix(native): drop use-measure for react-dom peerdep (#3323)
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett authored Aug 10, 2024
1 parent d542ace commit 3568191
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 13 deletions.
6 changes: 3 additions & 3 deletions example/src/demos/SVGRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
events,
ReconcilerRoot,
} from '@react-three/fiber'
import useMeasure, { Options as ResizeOptions } from 'react-use-measure'
import mergeRefs from 'react-merge-refs'
import { useMeasure, Options as ResizeOptions } from '../../../packages/fiber/src/web/use-measure'
import { SVGRenderer } from 'three-stdlib'

function TorusKnot() {
Expand Down Expand Up @@ -47,6 +46,7 @@ function Canvas({ children, resize, style, className, ...props }: Props) {

const [bind, size] = useMeasure({ scroll: true, debounce: { scroll: 50, resize: 0 }, ...resize })
const ref = React.useRef<HTMLDivElement>(null!)
React.useImperativeHandle(bind, () => ref.current, [])
const [gl] = useState(() => new SVGRenderer() as unknown as THREE.WebGLRenderer)
const root = React.useRef<ReconcilerRoot<HTMLElement>>(null!)

Expand All @@ -67,7 +67,7 @@ function Canvas({ children, resize, style, className, ...props }: Props) {

return (
<div
ref={mergeRefs([ref, bind])}
ref={ref}
className={className}
style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden', ...style }}
/>
Expand Down
3 changes: 2 additions & 1 deletion packages/fiber/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@
},
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/debounce": "^1.2.1",
"@types/react-reconciler": "^0.26.7",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"debounce": "^1.2.1",
"its-fine": "^1.0.6",
"react-reconciler": "^0.27.0",
"react-use-measure": "^2.1.1",
"scheduler": "^0.21.0",
"suspend-react": "^0.1.3",
"zustand": "^3.7.1"
Expand Down
3 changes: 1 addition & 2 deletions packages/fiber/src/web/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react'
import * as THREE from 'three'
import useMeasure from 'react-use-measure'
import type { Options as ResizeOptions } from 'react-use-measure'
import { useMeasure, Options as ResizeOptions } from './use-measure'
import { useContextBridge, FiberProvider } from 'its-fine'
import { isRef, SetBlock, Block, ErrorBoundary, useMutableCallback, useIsomorphicLayoutEffect } from '../core/utils'
import { ReconcilerRoot, extend, createRoot, unmountComponentAtNode, RenderProps } from '../core'
Expand Down
194 changes: 194 additions & 0 deletions packages/fiber/src/web/use-measure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useEffect, useState, useRef, useMemo } from 'react'
import createDebounce from 'debounce'

declare type ResizeObserverCallback = (entries: any[], observer: ResizeObserver) => void
declare class ResizeObserver {
constructor(callback: ResizeObserverCallback)
observe(target: Element, options?: any): void
unobserve(target: Element): void
disconnect(): void
static toString(): string
}

export interface RectReadOnly {
readonly x: number
readonly y: number
readonly width: number
readonly height: number
readonly top: number
readonly right: number
readonly bottom: number
readonly left: number
[key: string]: number
}

type HTMLOrSVGElement = HTMLElement | SVGElement

type Result = [(element: HTMLOrSVGElement | null) => void, RectReadOnly, () => void]

type State = {
element: HTMLOrSVGElement | null
scrollContainers: HTMLOrSVGElement[] | null
resizeObserver: ResizeObserver | null
lastBounds: RectReadOnly
}

export type Options = {
debounce?: number | { scroll: number; resize: number }
scroll?: boolean
polyfill?: { new (cb: ResizeObserverCallback): ResizeObserver }
offsetSize?: boolean
}

export function useMeasure(
{ debounce, scroll, polyfill, offsetSize }: Options = { debounce: 0, scroll: false, offsetSize: false },
): Result {
const ResizeObserver = polyfill || (typeof window !== 'undefined' && (window as any).ResizeObserver)

const [bounds, set] = useState<RectReadOnly>({
left: 0,
top: 0,
width: 0,
height: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
})

// In test mode
if (!ResizeObserver) {
// @ts-ignore
bounds.width = 1280
// @ts-ignore
bounds.height = 800
return [() => {}, bounds, () => {}]
}

// keep all state in a ref
const state = useRef<State>({ element: null, scrollContainers: null, resizeObserver: null, lastBounds: bounds })

// set actual debounce values early, so effects know if they should react accordingly
const scrollDebounce = debounce ? (typeof debounce === 'number' ? debounce : debounce.scroll) : null
const resizeDebounce = debounce ? (typeof debounce === 'number' ? debounce : debounce.resize) : null

// make sure to update state only as long as the component is truly mounted
const mounted = useRef(false)
useEffect(() => {
mounted.current = true
return () => void (mounted.current = false)
})

// memoize handlers, so event-listeners know when they should update
const [forceRefresh, resizeChange, scrollChange] = useMemo(() => {
const callback = () => {
if (!state.current.element) return
const { left, top, width, height, bottom, right, x, y } =
state.current.element.getBoundingClientRect() as unknown as RectReadOnly

const size = {
left,
top,
width,
height,
bottom,
right,
x,
y,
}

if (state.current.element instanceof HTMLElement && offsetSize) {
size.height = state.current.element.offsetHeight
size.width = state.current.element.offsetWidth
}

Object.freeze(size)
if (mounted.current && !areBoundsEqual(state.current.lastBounds, size)) set((state.current.lastBounds = size))
}
return [
callback,
resizeDebounce ? createDebounce(callback, resizeDebounce) : callback,
scrollDebounce ? createDebounce(callback, scrollDebounce) : callback,
]
}, [set, offsetSize, scrollDebounce, resizeDebounce])

// cleanup current scroll-listeners / observers
function removeListeners() {
if (state.current.scrollContainers) {
state.current.scrollContainers.forEach((element) => element.removeEventListener('scroll', scrollChange, true))
state.current.scrollContainers = null
}

if (state.current.resizeObserver) {
state.current.resizeObserver.disconnect()
state.current.resizeObserver = null
}
}

// add scroll-listeners / observers
function addListeners() {
if (!state.current.element) return
state.current.resizeObserver = new ResizeObserver(scrollChange)
state.current.resizeObserver!.observe(state.current.element)
if (scroll && state.current.scrollContainers) {
state.current.scrollContainers.forEach((scrollContainer) =>
scrollContainer.addEventListener('scroll', scrollChange, { capture: true, passive: true }),
)
}
}

// the ref we expose to the user
const ref = (node: HTMLOrSVGElement | null) => {
if (!node || node === state.current.element) return
removeListeners()
state.current.element = node
state.current.scrollContainers = findScrollContainers(node)
addListeners()
}

// add general event listeners
useOnWindowScroll(scrollChange, Boolean(scroll))
useOnWindowResize(resizeChange)

// respond to changes that are relevant for the listeners
useEffect(() => {
removeListeners()
addListeners()
}, [scroll, scrollChange, resizeChange])

Check warning on line 158 in packages/fiber/src/web/use-measure.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test

React Hook useEffect has missing dependencies: 'addListeners' and 'removeListeners'. Either include them or remove the dependency array

// remove all listeners when the components unmounts
useEffect(() => removeListeners, [])

Check warning on line 161 in packages/fiber/src/web/use-measure.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test

React Hook useEffect has a missing dependency: 'removeListeners'. Either include it or remove the dependency array
return [ref, bounds, forceRefresh]
}

// Adds native resize listener to window
function useOnWindowResize(onWindowResize: (event: Event) => void) {
useEffect(() => {
const cb = onWindowResize
window.addEventListener('resize', cb)
return () => void window.removeEventListener('resize', cb)
}, [onWindowResize])
}
function useOnWindowScroll(onScroll: () => void, enabled: boolean) {
useEffect(() => {
if (enabled) {
const cb = onScroll
window.addEventListener('scroll', cb, { capture: true, passive: true })
return () => void window.removeEventListener('scroll', cb, true)
}
}, [onScroll, enabled])
}

// Returns a list of scroll offsets
function findScrollContainers(element: HTMLOrSVGElement | null): HTMLOrSVGElement[] {
const result: HTMLOrSVGElement[] = []
if (!element || element === document.body) return result
const { overflow, overflowX, overflowY } = window.getComputedStyle(element)
if ([overflow, overflowX, overflowY].some((prop) => prop === 'auto' || prop === 'scroll')) result.push(element)
return [...result, ...findScrollContainers(element.parentElement)]
}

// Checks if element boundaries are equal
const keys: (keyof RectReadOnly)[] = ['x', 'y', 'top', 'bottom', 'left', 'right', 'width', 'height']
const areBoundsEqual = (a: RectReadOnly, b: RectReadOnly): boolean => keys.every((key) => a[key] === b[key])
12 changes: 5 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2815,6 +2815,11 @@
dependencies:
"@babel/types" "^7.3.0"

"@types/debounce@^1.2.1":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.4.tgz#cb7e85d9ad5ababfac2f27183e8ac8b576b2abb3"
integrity sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==

"@types/draco3d@^1.4.0":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/draco3d/-/draco3d-1.4.2.tgz#7faccb809db2a5e19b9efb97c5f2eb9d64d527ea"
Expand Down Expand Up @@ -8911,13 +8916,6 @@ react-test-renderer@^18.0.0:
react-shallow-renderer "^16.13.1"
scheduler "^0.21.0"

react-use-measure@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.1.1.tgz#5824537f4ee01c9469c45d5f7a8446177c6cc4ba"
integrity sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==
dependencies:
debounce "^1.2.1"

react-use-refs@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/react-use-refs/-/react-use-refs-1.0.1.tgz#44cab5f4764b3fa4a112189c0058fc8752d1eb2c"
Expand Down

0 comments on commit 3568191

Please sign in to comment.