-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(dnd): add useDynamicDescription to prevent unnecessary re-renders #9738
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,10 +12,32 @@ | |
|
|
||
| import {AriaLabelingProps} from '@react-types/shared'; | ||
| import {useLayoutEffect} from './useLayoutEffect'; | ||
| import {useState} from 'react'; | ||
| import {useRef, useState} from 'react'; | ||
|
|
||
| let descriptionId = 0; | ||
| const descriptionNodes = new Map<string, {refCount: number, element: Element}>(); | ||
| const descriptionNodes = new Map<string, {refCount: number, element: HTMLElement}>(); | ||
| const dynamicDescriptionNodes = new Map<string, {refCount: number, element: HTMLElement}>(); | ||
|
|
||
| function createDescriptionNode(id: string, description: string): HTMLElement { | ||
| let node = document.createElement('div'); | ||
| node.id = id; | ||
| node.style.display = 'none'; | ||
| node.textContent = description; | ||
| document.body.appendChild(node); | ||
| return node; | ||
| } | ||
|
|
||
| function getOrCreateDynamicDescriptionNode(descriptionKey: string) { | ||
| let desc = dynamicDescriptionNodes.get(descriptionKey); | ||
| if (!desc) { | ||
| let id = `react-aria-description-${descriptionId++}`; | ||
| let node = createDescriptionNode(id, ''); | ||
| desc = {refCount: 0, element: node}; | ||
| dynamicDescriptionNodes.set(descriptionKey, desc); | ||
| } | ||
|
|
||
| return desc; | ||
| } | ||
|
|
||
| export function useDescription(description?: string): AriaLabelingProps { | ||
| let [id, setId] = useState<string | undefined>(); | ||
|
|
@@ -30,11 +52,7 @@ export function useDescription(description?: string): AriaLabelingProps { | |
| let id = `react-aria-description-${descriptionId++}`; | ||
| setId(id); | ||
|
|
||
| let node = document.createElement('div'); | ||
| node.id = id; | ||
| node.style.display = 'none'; | ||
| node.textContent = description; | ||
| document.body.appendChild(node); | ||
| let node = createDescriptionNode(id, description); | ||
| desc = {refCount: 0, element: node}; | ||
| descriptionNodes.set(description, desc); | ||
| } else { | ||
|
|
@@ -54,3 +72,66 @@ export function useDescription(description?: string): AriaLabelingProps { | |
| 'aria-describedby': description ? id : undefined | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Provides a stable `aria-describedby` id for descriptions that change over time. | ||
| * Unlike `useDescription`, this shares a hidden element by `descriptionKey` | ||
| * and updates its text content in place so many consumers can reuse one id. | ||
| */ | ||
| export function useDynamicDescription(description: string | undefined, descriptionKey: string): AriaLabelingProps { | ||
| let [id, setId] = useState<string | undefined>(); | ||
| let subscriptionRef = useRef<{key: string, desc: {refCount: number, element: HTMLElement}} | null>(null); | ||
| let nodeRef = useRef<HTMLElement | null>(null); | ||
|
|
||
| useLayoutEffect(() => { | ||
| let subscription = subscriptionRef.current; | ||
| if (subscription && subscription.key !== descriptionKey) { | ||
| if (--subscription.desc.refCount === 0) { | ||
| subscription.desc.element.remove(); | ||
| dynamicDescriptionNodes.delete(subscription.key); | ||
| } | ||
|
|
||
| subscriptionRef.current = null; | ||
| if (nodeRef.current === subscription.desc.element) { | ||
| nodeRef.current = null; | ||
| } | ||
| subscription = null; | ||
| } | ||
|
|
||
| if (!subscription && description) { | ||
| let desc = getOrCreateDynamicDescriptionNode(descriptionKey); | ||
| desc.refCount++; | ||
| subscriptionRef.current = {key: descriptionKey, desc}; | ||
| nodeRef.current = desc.element; | ||
| setId(desc.element.id); | ||
| desc.element.textContent = description; | ||
| } | ||
| }, [descriptionKey, description]); | ||
|
|
||
| useLayoutEffect(() => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the purpose of this effect? i can't quite figure it out, maybe it should also be merged into the effect above? |
||
| if (description && nodeRef.current) { | ||
| nodeRef.current.textContent = description; | ||
| } | ||
| }, [description]); | ||
|
|
||
| useLayoutEffect(() => { | ||
| return () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you move this up to be in the first useLayoutEffect, then you can de-duplicate the refCount removal, always handle it in the cleanup of that effect |
||
| let subscription = subscriptionRef.current; | ||
| if (subscription) { | ||
| if (--subscription.desc.refCount === 0) { | ||
| subscription.desc.element.remove(); | ||
| dynamicDescriptionNodes.delete(subscription.key); | ||
| } | ||
|
|
||
| subscriptionRef.current = null; | ||
| if (nodeRef.current === subscription.desc.element) { | ||
| nodeRef.current = null; | ||
| } | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| return { | ||
| 'aria-describedby': description ? id : undefined | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the case that multiple copies are loaded, this could create conflicting ids, better to use id generation like
crypto.randomUUIDThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or put the id generation into the hooks and make use of
useId