React组件挂载时访问浏览器API的正确方案探讨:useState、普通变量、useRef与useSyncExternalStore对比
Great question! I’ve run into this exact scenario when working with Portals and multi-instance components too. Let’s break down the correct, React-compliant approach, plus why your previous attempts had issues.
The Correct Solution: Client-Side Only Rendering
You don’t need to manually track "mounted" state to use Portals. The core issue here is avoiding browser API access during server-side rendering (SSR) and ensuring multi-instance components behave predictably. Here are two solid approaches:
Option 1: Explicit Client/Server Check
This is the simplest approach if your component doesn’t need to render anything on the server:
import { createPortal } from 'react-dom'; export default function Component() { // Skip Portal rendering on the server (where `window` doesn't exist) if (typeof window === 'undefined') { return null; // Or a loading placeholder if needed } return createPortal( <> {/* Your Portal content goes here */} </>, document.body ); }
- Works for multi-instances: Each component instance evaluates this check independently, no shared state issues.
- SSR-safe: Never tries to access
documenton the server. - No extra re-renders: Directly returns the Portal once in the client.
Option 2: useState + useEffect for Hydration Safety
If you need to avoid SSR hydration mismatches (e.g., your server renders a placeholder that matches the client’s initial state), use this approach:
import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; export default function Component() { const [isClient, setIsClient] = useState(false); useEffect(() => { // This effect only runs in the client, after the component mounts setIsClient(true); }, []); // Server renders null, client renders Portal after mount if (!isClient) { return null; } return createPortal( <> {/* Your Portal content goes here */} </>, document.body ); }
- Hydration-friendly: Ensures the server and client render the same initial content (
null) before the Portal loads. - Multi-instance safe: Each instance has its own
isClientstate. - Follows React rules: State updates happen in
useEffect(not during rendering), so no unexpected behavior.
Why Your Previous Attempts Had Issues
Let’s walk through why each of your test solutions fell short:
1. useState + Empty useEffect
While this technically works, you’re right that it triggers a cascading re-render. However, it’s not a strict "anti-pattern" in this specific case (since it’s a one-time update), but the client/server check above is cleaner and avoids the extra render.
2. External Variable
This is a hard no. Shared external variables break component purity—when you have multiple instances, one instance’s isMount = true will affect all others. This leads to unpredictable behavior, especially if components mount/unmount at different times.
3. useRef During Render
React explicitly forbids reading ref.current during rendering because refs are meant for values that don’t affect the render output. Accessing them during render can cause stale values or unexpected updates, which is why you saw the error. Refs should only be used in event handlers or effects.
4. useSyncExternalStore
This hook is designed for syncing React components with external data sources (like Redux or browser APIs that emit events). Your implementation works, but it’s an abuse of the hook—you’re not syncing any real external state, just using it as a hack to get client/server differentiation. It’s unreadable and goes against the hook’s intended use.
Final Takeaway
For any scenario where you need to access browser APIs (like Portals), focus on client/server differentiation rather than tracking "mounted" state. The two options above are compliant with React’s rules, work for multi-instance components, and avoid the pitfalls of your earlier attempts.
内容的提问来源于stack exchange,提问作者Denis Kramarenko

