Displaying a Toolbar with React Portal, only when there are children

73 Views Asked by At

GOAL: Display the ToolkitArea only when the "#toolkitArea" has children

PROBLEM: I can't count the current amount of children inside the ToolkitArea for some reason

WHAT I DID SO FAR:

I have a component "Toolbar" and i want it to be displayed when Buttons are added via ReactPortal.

My current approach is having a UseEffect triggered inside the toolkitArea, checking for the amount of childrens inside the area.

import {  faEyeSlash, faEye } from '@fortawesome/free-solid-svg-icons'
import ToolkitButton from './ToolkitButton'
import { useState } from 'react'
export interface ToolkitAreaProps {
  children?: React.ReactNode

}
const ToolkitArea = ({ children }:ToolkitAreaProps) => {
  const [visible, setVisible] = useState<boolean>(true)
  const [isEmpty, setEmpty] = useState<boolean>(false)
  const toolkit = useRef<HTMLDivElement>(null)
  useEffect(() => {
    const toolkitArea = (document.getElementById('toolkitArea') as HTMLDivElement)
    console.log(toolkitArea.children, '', toolkit.current?.children, toolkit.current?.children.length)
    if(toolkitArea.children.length > 0)
      setEmpty(false)
    else setEmpty(true)
  }, [toolkit])

  return <div className="fixed bottom-6 w-full h-16 z-40 px-8 md:pl-32 md:pr-16 flex">
    <div className={`-ml-4 md:-ml-12 rounded-md w-16 h-16 bg-indigo-700 flex items-center justify-center shadow shadow-black shadow-md ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
      <ToolkitButton icon={faEye} onClick={() => setVisible(true)} tooltip={'Show Toolbar'}/>
    </div>
    <div className={`h-full mx-auto bg-indigo-700 w-fit max-w-full rounded-md shadow shadow-black shadow-md py-2 flex px-4 overflow-x-auto md:overflow-x-visible ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
      <ToolkitButton icon={faEyeSlash} onClick={() => setVisible(false)} className="-order-1" tooltip={'Toggle Toolbar'}/>
      <div ref={toolkit} className={'w-fit flex gap-4 h-full pl-4'} id={'toolkitArea'}>

      </div>
    </div>
  </div>
} 

export default ToolkitArea

The output in the browser looks like this: Console.log collapsed Console.log expanded

It says the children.length is 0, even when there are 5 elements displayed.

I have no clue what is responsible for this behavior. I can only imagine, the Buttons are added after the useEffect triggers, and the console.log inside the browser is adapted.

How can I achieve my goal, to only display the whole toolkitArea, when there are children inside the toolkitArea?

1

There are 1 best solutions below

0
motto On

In general, React is not going to re-render your ToolkitArea component when the portal contents change, so the useEffect hook won't be triggered. Even if it did, you may not be able to depend on the rendering order to have an accurate view of how many children the #toolkitArea portal contains.

I would suggest using some slightly heavy machinery in the form of the HTML5 MutationObserver, and run a callback in your ToolkitArea whenever there's a change to the contents of the #toolkitArea div.

You can wrap the necessary logic in a hook:

function useEmptyDetector(element: HTMLDivElement | null): boolean
{
  // Watch the given element and return true when it has no children

  const [isEmpty, setIsEmpty] = useState(!element || element.childElementCount === 0);

  useEffect(() => {
    if (element) {
      // Watch for changes to the child list, and update isEmpty accordingly
      const observer = new MutationObserver(() => {
        setIsEmpty(element.childElementCount === 0)
      });
      observer.observe(element, { childList: true });

      return () => observer.disconnect();  // cleanup when unmounted
    }
    else setIsEmpty(true);
  }, [element, setIsEmpty]);

  return isEmpty;
}

Sandbox

The hook returns a value isEmpty which you can then use in your component (note the use of a callback ref for #toolkitArea instead of the ref object):

const ToolkitArea = ({ children }:ToolkitAreaProps) => {
  const [visible, setVisible] = useState<boolean>(true)
  const [toolkit, setToolkit] = useState<HTMLDivElement>();
  const isEmpty = useEmptyDetector(toolkit);

  return <div className="fixed bottom-6 w-full h-16 z-40 px-8 md:pl-32 md:pr-16 flex">
    <div className={`-ml-4 md:-ml-12 rounded-md w-16 h-16 bg-indigo-700 flex items-center justify-center shadow shadow-black shadow-md ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
      <ToolkitButton icon={faEye} onClick={() => setVisible(true)} tooltip={'Show Toolbar'}/>
    </div>
    <div className={`h-full mx-auto bg-indigo-700 w-fit max-w-full rounded-md shadow shadow-black shadow-md py-2 flex px-4 overflow-x-auto md:overflow-x-visible ${(visible || isEmpty) ? 'invisible' : 'visible'}`}>
      <ToolkitButton icon={faEyeSlash} onClick={() => setVisible(false)} className="-order-1" tooltip={'Toggle Toolbar'}/>
      <div ref={setToolkit} className={'w-fit flex gap-4 h-full pl-4'} id={'toolkitArea'}>

      </div>
    </div>
  </div>
}