最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React: HTML Details toggles uncontrollably when starts open - Stack Overflow

programmeradmin2浏览0评论

I'm trying to use the HTML <details> tag to create a simple expandable section using semantic html in bination with React.

The <details><summary></summary></details> behaviour works great out of the box and for the 1-2% of my users that use IE users that don't get the show-hide nature of the content, it really isn't the end of the world for the content to always be shown for the time being.

My issue es when using React hooks to hold onto whether the <details> panel is open or closed. The basic layout of the React ponent is as follows:

const DetailsComponent = ({startOpen}) => {
  const [open, toggleOpen] = useState(startOpen);

  return (
    <details onToggle={() => toggleOpen(!open)} open={open}>
      <summary>Summary</summary>
      <p>Hidden content hidden content hidden content</p>
    </details>
  );
};

The reason I need to use the onToggle event is to update the open state variable to trigger some other javascript in my real world example. I use the startOpen prop to decide whether on page render whether the details pane is open or closed.

The expected behaviour happens when I use the ponent as, <DetailsComponent startOpen={ false } />.

However, when is want to start with the pane open on load (<DetailsComponent startOpen={ true } />), I can visibly see the pane opening and closing very very quickly over and over again forever.

I'm trying to use the HTML <details> tag to create a simple expandable section using semantic html in bination with React.

The <details><summary></summary></details> behaviour works great out of the box and for the 1-2% of my users that use IE users that don't get the show-hide nature of the content, it really isn't the end of the world for the content to always be shown for the time being.

My issue es when using React hooks to hold onto whether the <details> panel is open or closed. The basic layout of the React ponent is as follows:

const DetailsComponent = ({startOpen}) => {
  const [open, toggleOpen] = useState(startOpen);

  return (
    <details onToggle={() => toggleOpen(!open)} open={open}>
      <summary>Summary</summary>
      <p>Hidden content hidden content hidden content</p>
    </details>
  );
};

The reason I need to use the onToggle event is to update the open state variable to trigger some other javascript in my real world example. I use the startOpen prop to decide whether on page render whether the details pane is open or closed.

The expected behaviour happens when I use the ponent as, <DetailsComponent startOpen={ false } />.

However, when is want to start with the pane open on load (<DetailsComponent startOpen={ true } />), I can visibly see the pane opening and closing very very quickly over and over again forever.

Share Improve this question asked Nov 19, 2019 at 20:50 Joel BiffinJoel Biffin 3781 gold badge6 silver badges22 bronze badges
Add a ment  | 

5 Answers 5

Reset to default 6

I had the same problem. I eventually worked out I can monitor the current state by handling onToggle and just not use it to set the open attribute.

export default ({ defaultOpen, summary, children }: DetailsProps): JSX.Element => {
  const [expanded, setExpanded] = useState(defaultOpen || false);

  return (
    <details open={defaultOpen} onToggle={e => setExpanded((e.currentTarget as HTMLDetailsElement).open)}>
      <summary>
        {expanded ? <ExpandedIcon /> : <CollapsedIcon />}
        {summary}
      </summary>
      {children}
    </details>
  );
};

Because defaultOpen does not change it does not cause a DOM update, so the HTML control is still in charge of its state.

I think You should use prevState

<details onToggle={() => toggleOpen(prevOpen => !prevOpen )} open={open}>

The <details> HTML element does not need to be controlled with js because it already has the functionality to be opened and closed. When you pass the open attribute and change it in the ontoggle event, you are creating an endless event loop because the element is toggled then the open state changes which toggles the element which triggers the ontoggle event and so on... The only thing you need is to pass the initial open state.

const DetailsComponent = ({startOpen}) => {
  return (
    <details open={startOpen}>
      <summary>Summary</summary>
      <p>Hidden content hidden content hidden content</p>
    </details>
  );
};

Seems like onToggle is called before mount and that's causing an endless loop for the case where it is rendered open. Because that triggers a new toggle event.

One way to avoid it, is to check if the details tag is mounted and only toggle once it is mounted. That way you're ignoring the first toggle event.

const DetailsComponent = ({ startOpen }) => {
  const [open, toggleOpen] = useState(startOpen);
  const [isMounted, setMount] = useState(false);

  useEffect(() => {
    setMount(true);
  }, []);

  return (
    <details onToggle={() => isMounted && toggleOpen(!open)} open={open}>
      <summary>Summary</summary>
      <p>Hidden content hidden content hidden content</p>
    </details>
  );
};

You can find a working demo in this Codesandbox.

There are couple of things to consider.

First, when calling onClick on details HTML element, we are reading the current open state before the change is performed, i.e. we are reading an outdated state. According to documentation, it is better to use toggle event which is fired after the open state is changed:

const detailsRef = useRef<HTMLDetailsElement>(null)

const onToggleCallback = useCallback(() => {
  console.log(detailsRef.current?.open)
}, [])

useEffect(() => {
  detailsRef.current?.addEventListener('toggle', onToggleCallback)

  return () => {
    detailsRef.current?.removeEventListener('toggle', onToggleCallback)
  }
}, [onToggleCallback])

// [...] details

Second, whenever React state isOpen is changed and this variable is passed down into details element, the toggle event is fired again. To prevent changing the React state twice and ending up in a state mismatch, we can simply pare the two values and save a new value into the React state only in case it differs:

const [isOpen, setIsOpen] = useState(true)

// [...] useRef

const onToggleCallback = useCallback(() => {
  const newValue = detailsRef.current?.open
  
  if (newValue !== undefined && newValue !== isOpen) {
    setIsOpen(newValue)
  }
}, [])

// [...] useEffect

<details ref={detailsRef} open={isOpen}>
  <summary>Open details</summary>
  <div>Details are opened</div>
</details>

Fully working example can be found here https://playcode.io/1559702

发布评论

评论列表(0)

  1. 暂无评论