React自定义Hook中布局重渲染后如何正确传递useRef?
问题背景
需要实现React组件中某一区域的全屏切换功能(类似YouTube视频全屏),通过组件角落的按钮切换状态。组件依赖React Context传递的数据,需等待数据加载完成后才会渲染包含目标DOM元素的完整组件。封装了自定义Hook useFullScreen 实现全屏逻辑,但遇到ref无法正确传递给Hook的问题——Hook始终获取不到viewerRef对应的DOM元素,尽管在组件的useEffect中能正常拿到该ref。
现有代码
useFullScreen.tsx
import * as React from "react"; type useFullScreenProps = { ref: React.MutableRefObject<null | HTMLDivElement>; }; const useFullScreen = ({ref} : useFullScreenProps) => { const [isFullScreen, setIsFullScreen] = React.useState(false); const enterFullScreen = () => { if (ref.current) { ref.current.requestFullscreen(); } }; const exitFullScreen = () => { if (document.fullscreenElement) { document.exitFullscreen(); } }; const toggleFullScreen = () => { isFullScreen ? exitFullScreen() : enterFullScreen(); }; React.useEffect(() => { const handleFullscreenChange = () => { setIsFullScreen(document.fullscreenElement !== null); }; document.addEventListener("fullscreenchange", handleFullscreenChange); return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); return [isFullScreen, toggleFullScreen]; }; export default useFullScreen;
ViewerComponent.tsx
import useFullScreen from "./Common/useFullScreen"; const ViewerComponent = () => { let viewerRef = React.useRef<HTMLDivElement>(null); const [isFullScreen, toggleFullScreen] = useFullScreen(viewerRef as any); const viewData = useClientObject<ExamData>(React.useContext(ClientContext), "viewer_data"); if (viewData === undefined || Object.keys(viewData).length === 0) { return <h4> Loading page</h4> } return ( <div id="view-workspace" ref={viewerRef}> <button onClick={toggleFullScreen as () => void}> {isFullScreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} </button> <div> {/* Rest of the page goes here */} </div> </div> ) };
问题原因
核心问题是闭包陷阱:enterFullScreen 和 toggleFullScreen 函数在Hook初始化时创建,此时ref.current为null(因为组件还在加载状态,目标DOM未渲染)。后续数据加载完成、DOM渲染后,ref.current更新为真实DOM元素,但这些函数的闭包中捕获的仍是初始的ref引用(current为null),导致调用enterFullScreen时无法获取到最新的DOM元素。
另外,组件中用let声明viewerRef不符合React最佳实践,应该用const,因为useRef返回的是稳定的引用,不需要重新赋值。
解决方案
1. 优化自定义Hook,确保函数能获取最新的ref
使用React.useCallback包裹enterFullScreen和toggleFullScreen,将ref加入依赖数组,这样每次ref变化时,函数会重新创建,从而捕获最新的ref.current。
修改后的useFullScreen.tsx:
import * as React from "react"; type useFullScreenProps = { ref: React.MutableRefObject<null | HTMLDivElement>; }; const useFullScreen = ({ref} : useFullScreenProps) => { const [isFullScreen, setIsFullScreen] = React.useState(false); // 用useCallback包裹,依赖ref确保获取最新的current const enterFullScreen = React.useCallback(() => { if (ref.current) { ref.current.requestFullscreen(); } }, [ref]); const exitFullScreen = React.useCallback(() => { if (document.fullscreenElement) { document.exitFullscreen(); } }, []); // 依赖enterFullScreen和isFullScreen,确保切换逻辑用最新的函数 const toggleFullScreen = React.useCallback(() => { isFullScreen ? exitFullScreen() : enterFullScreen(); }, [isFullScreen, enterFullScreen, exitFullScreen]); React.useEffect(() => { const handleFullscreenChange = () => { setIsFullScreen(document.fullscreenElement !== null); }; document.addEventListener("fullscreenchange", handleFullscreenChange); return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); return [isFullScreen, toggleFullScreen] as const; }; export default useFullScreen;
2. 调整组件中的ref声明和Hook调用
将let viewerRef改为const,并移除不必要的类型断言:
修改后的ViewerComponent.tsx:
import useFullScreen from "./Common/useFullScreen"; const ViewerComponent = () => { const viewerRef = React.useRef<HTMLDivElement>(null); const [isFullScreen, toggleFullScreen] = useFullScreen({ ref: viewerRef }); const viewData = useClientObject<ExamData>(React.useContext(ClientContext), "viewer_data"); if (viewData === undefined || Object.keys(viewData).length === 0) { return <h4> Loading page</h4> } return ( <div id="view-workspace" ref={viewerRef}> <button onClick={toggleFullScreen}> {isFullScreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} </button> <div> {/* Rest of the page goes here */} </div> </div> ) };
额外优化:处理全屏API的兼容性(可选)
如果需要兼容更多浏览器,可以在enterFullScreen中添加前缀兼容:
const enterFullScreen = React.useCallback(() => { const element = ref.current; if (!element) return; if (element.requestFullscreen) { element.requestFullscreen(); } else if ((element as any).webkitRequestFullscreen) { (element as any).webkitRequestFullscreen(); } else if ((element as any).mozRequestFullScreen) { (element as any).mozRequestFullScreen(); } else if ((element as any).msRequestFullscreen) { (element as any).msRequestFullscreen(); } }, [ref]);
内容的提问来源于stack exchange,提问作者Showcaselfloyd

