Skip to content
Advertisement

How do I render a react portal modal with different children?

Right now, I have a react portal rendering a ‘pinnable’ side drawer modal that will display based on a redux state. The contents of the modal will have information based on where that modal was pinned from, in this case my notifications.

The problem I’m running into at the moment is that since the modal will be pinnable in multiple places, I’m not exactly sure of the logic on how to handle the modal contents if the modal is already pinned.

I’ve tried/considered the following:

  1. Just have one portal render its children dynamically. Unfortunately the location of where the portal will be rendered does not contain the contents and logic of the modal, so I believe this can’t be done.
  2. Compare props.children and if they’re not identical, render the newer portal and deconstruct the other. I’m hesitant to use this approach since I believe there’s a better solution out there.
  3. Render the portals based on Ids and deconstruct/reconstruct where needed if one exists. I’m leaning towards this approach, but again I’d like to see if there’s a better one.

Portal location:

export default function PaperContainer() {


return <div id="pinned-container"></div>;

}

Portal:

export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);

return (
    pinned &&
    createPortal(            
            <div>
                <div>{props.children}</div>
            </div>
        ,
        document.getElementById('pinned-container')
    )
);
}

Where the portals are called (simplified for brevity):

export default function PortalCallLocationOne() {
    const dispatch = useDispatch();
    const pinContainer = () => {
         dispatch(toggleDrawer());
    };

 return  (
    <>
         <Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
                 Pin Notification
         </Button>
         <PinnedContainer>
               //Notification
         </PinnedContainer>
    </>
   );
}

export default function PortalCallLocationTwo() {
     const dispatch = useDispatch();
     const pinContainer = () => {
         dispatch(toggleDrawer());
     };
     return (
<>
        <Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
                 Pin List
        </Button>
       <PinnedContainer>
          // List
       </PinnedContainer>
     );
</>
   }

Advertisement

Answer

I tried going off of #3 and destroying pinned-container‘s first child if it existed and replace it with the new children. This didn’t work since React was expecting that child and kept throwing failed to execute removeChild on node errors.

Unfortunately it looks like react is unable to replace portal children instead of appending them.

However, I was able to solve my issue by unpinning the portal and repinning it with redux actions.

export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);

useEffect(() => {
    if (pinned) {
         dispatch(clearPinned());
         dispatch(pinDrawer(true));
      }
    }, []);

return (
    pinned &&
    createPortal(            
            <div>
                <div>{props.children}</div>
            </div>
        ,
        document.getElementById('pinned-container')
    )
);
}

Reducer:

export const initialState = {
    isDrawerPinned: false,
}

export const reducer = (state = initialState, action) => {
     switch(action.type){
       case actionTypes.PIN_DRAWER:
            return {
                ...state,
                isDrawerPinned: action.isPinned ? action.isPinned : !state.isDrawerPinned,
            };
    case actionTypes.CLEAR_PINNED:
            return {
                ...state,
                isDrawerPinned: state.isDrawerPinned ? !state.isDrawerPinned : state.isDrawerPinned
            };

}

}
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement