Skip to content
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

fix(native): inline use-measure for react-dom peerdep #3323

Merged
merged 1 commit into from
Aug 10, 2024
Merged
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
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