如何在Portal封装组件中合规条件调用React useRef并优化代码?
Great work building a Portal component that checks all your requirement boxes—handling multiple instances, targeting specific parent elements, and cleaning up properly is no small win. You’re totally right to flag the hook rule violation in your current code: even if you don’t see warnings now, calling useRef inside a ternary breaks React’s Rules of Hooks, which depend on consistent hook call order across renders. This could lead to subtle, hard-to-debug bugs later on, especially in more complex component trees or different build environments.
Let’s refactor your component to fix this issue while keeping all your desired functionality intact:
Refactored Portal Component
import * as React from 'react'; import { createPortal } from 'react-dom'; export interface PortalProps { children: React.ReactElement; parentId?: string; } export const Portal = ({ children, parentId }: PortalProps): React.ReactElement | null => { // Declare ref at the component top level (fully compliant with hook rules) const containerRef = React.useRef<HTMLElement | null>(null); React.useEffect(() => { // Clean up any auto-created container from previous renders if (containerRef.current && !parentId) { document.body.removeChild(containerRef.current); } // Set up the correct container based on parentId if (parentId) { // Target an existing DOM element by ID containerRef.current = document.getElementById(parentId); } else { // Create a new div and attach it to the document body const newContainer = document.createElement('div'); containerRef.current = newContainer; document.body.appendChild(newContainer); } // Cleanup function: runs on component unmount or when parentId changes return () => { // Only remove auto-created containers (never delete user-provided parent elements) if (containerRef.current && !parentId) { document.body.removeChild(containerRef.current); } containerRef.current = null; }; }, [parentId]); // Re-run the effect whenever parentId changes // Guard clause: avoid rendering the portal if the container isn't ready if (!containerRef.current) { return null; } return createPortal(children, containerRef.current); }; export default Portal;
Key Improvements
- Hook Rule Compliance: All hooks are called at the component’s top level, so React can reliably track the ref state across renders.
- Robust Cleanup: The cleanup function handles both component unmount and
parentIdchanges—if you switch between targeting a specific parent and using an auto-created container, we’ll clean up the old container before setting up the new one. - Safety Checks: Added a guard clause to prevent passing
nulltocreatePortal, which avoids potential errors if the target element doesn’t exist yet. - Unchanged Functionality: All your original use cases work exactly as before:
- Multiple Portal instances coexist without interference (each has its own independent
containerRef). - Targeting a parent via
parentIdworks without needing to pass ref props. - Omitting
parentIdauto-creates a container in the document body.
- Multiple Portal instances coexist without interference (each has its own independent
Quick Additional Tip
If your target parent element might be dynamically added to the DOM after the Portal mounts, you could extend this component with a MutationObserver to watch for the element’s existence. But for most static or pre-existing parent elements, the refactored code above will serve you perfectly.
内容的提问来源于stack exchange,提问作者Aaron Balthaser

