解开onMouseEnter、onMouseLeave和onClick中的状态变化

5

我有以下组件

  const ListItem = ({ children, title = '' }) => {
    const [isActive, setIsActive] = useState(false)

    useEffect(() => {
      console.log('isActive', isActive)
    }, [isActive])

    return (
      <li className={`depth1${(isActive ? ' is-active' : '')}`} onMouseEnter={() => {
        console.log('onMouseEnter')
        setIsActive(true)
      }} onMouseLeave={() => {
        console.log('onMouseLeave')
        setIsActive(false)
      }}>
        <a href="#" onClick={(e) => {
          console.log('onClick a')
          e.preventDefault()
          setIsActive(!isActive)
        }}>{title}</a>
        {isActive && (
          <ul onClick={() => {
            console.log('onClick ul')
            setIsActive(!isActive)
          }}>
            {children}
          </ul>
        )}
      </li>
    )
  }

该组件用于以下场景。
<ul>
  <ListItem title="BRAND">
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
  </ListItem>
  <ListItem title="SERVICE">
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
  </ListItem>
  <ListItem title="CLIENT">
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
  </ListItem>
  <ListItem title="CONTENTS">
    <li>
      <a href="#" onClick={(e) => {
        e.preventDefault()
      }}>Link</a>
    </li>
  </ListItem>
</ul>

在我的电脑上,当鼠标悬停在一个 ListItem 上时,会触发 onMouseEnter 并在控制台中记录日志。
onMouseEnter
isActive true

在我的电脑上,当鼠标离开一个 ListItem 时,onMouseLeave 就会被触发并在控制台中记录日志。
onMouseLeave
isActive false

这里所有的东西都运行正常。问题出现在移动设备上,当我打开导航菜单时,我会在控制台中记录日志。

isActive false

正如预期。

但是当我第一次点击ListItem时,我会在控制台打印日志

onMouseEnter
isActive true
onClick a
isActive false

我的 ListItemchildren 没有打开。但当我第二次点击 ListItem 时,控制台记录了日志。

onClick a
isActive true

当我第三次点击 ListItem 时,它将打开我的 ListItem children。 我会在控制台上记录日志。

onClick a
isActive false

这会关闭我的 ListItemchildren

我如何解决状态变化的混乱,以便在移动设备上第一次而不是第二次点击时打开我的 ListItemchildren

2个回答

0

这是一个令人惊讶的棘手问题。你可以尝试几种方法。我自己还没有时间去尝试。

  • 方法1: 您可以在<li>元素上使用本机非冒泡事件(mouseenter,mouseleave)。使使用本机事件更方便的钩子是useEvent(只需复制代码,因为我还没有确定该库的未来)
import React, { useState, useCallback } from 'react';
import classnames from 'classnames';
import { useEvent } from 'the/path/to/the/hook';

const useMouseEnter = (listener, options) => useEvent('mouseenter', listener, options);
const useMouseLeave = (listener, options) => useEvent('mouseleave', listener, options);

const ListItem = ({ children, title = '' }) => {

  const [isActive, setIsActive] = useState(false);
  const toggle = () => setIsActive(_ => !_);
  const open   = () => setIsActive(true);
  const close  = () => setIsActive(false);

  const [setMouseEnterTarget] = useMouseEnter(open); // the setters are stable...
  const [setMouseLeaveTarget] = useMouseLeave(close);
  const setRef = useCallback((element) => {
    setMouseEnterTarget(element);
    setMouseLeaveTarget(element);
  }, []); // ...so we don't need them in the dependencies array

  return (
      <li ref={setRef} className={classnames('depth1', { 'is-active': isActive })}>
        <a href="#" onClick={(e) => {
          console.log('onClick a');
          e.preventDefault();
          toggle();
        }}>{title}</a>

方法二: 您可以检测当前环境中是否支持“hover”功能,并相应地控制代码的行为:
  const [isActive, setIsActive] = useState(false);
  const toggle = () => setIsActive(_ => !_);
  const open   = () => setIsActive(true);
  const close  = () => setIsActive(false);
  const supportsHover = useMedia('(hover: hover)');
  const mouseEvents = {
    onMouseEnter: open,
    onMouseLeave: close,
  };

  return (
      <li className={classnames('depth1', { 'is-active': isActive })}
        { ...supportsHover ? mouseEvents : undefined }
      >
        <a href="#" onClick={(e) => {
          console.log('onClick a');
          e.preventDefault();
          toggle();
        }}>{title}</a>

和钩子

import { useState, useEffect } from 'react';

export const useMedia = (query) => {
  const [matches, setMatches] = useState(window.matchMedia(query).matches);

  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) setMatches(media.matches);
    const listener = () => setMatches(media.matches);
    media.addListener(listener);
    return () => media.removeListener(listener);
  }, [matches, query]);

  return matches;
};
  • 方法3: 您可以在每次切换后启动一个计时器,例如250毫秒,并忽略进一步的切换直到计时器过期

  • 方法4: 您可以通过同步设置标志并异步删除标志(因此在下一个事件循环迭代中)来防止状态被切换超过一次event loop(但我不知道问题事件是否在移动设备上同步触发)


0

我发现解开这些状态非常困难,所以我采用了以下方法来解决问题。

const [isMobileNav, setIsMobileNav] = useState(false)

const windowWidthOnLoad = () => {
  const browser = window.innerWidth
  if (browser <= 1024) {
    setIsMobileNav(true)
  } else {
    setIsMobileNav(false)
  }
}

const windowWidthOnResize = () => {
  const browser = window.innerWidth
  if (browser <= 1024) {
    setIsMobileNav(true)
  } else {
    setIsMobileNav(false)
  }
}

useEffect(() => {
  windowWidthOnLoad()
  window.addEventListener('resize', windowWidthOnResize)
}, [])

const ListItem = ({ children, title = '' }) => {
  const [isActive, setIsActive] = useState(false)

  return (
    <>
      {/* click link once to show ListItem children on mobile */}
      {isMobileNav
        ? <li className={`depth1${(isActive ? ' is-active' : '')}`}>
          <a href="#" onClick={(e) => {
            e.preventDefault()
            setIsActive(!isActive)
          }}>{title}</a>
          {isActive && (
            <ul onClick={() => {
              setIsActive(!isActive)
            }}>
              {children}
            </ul>
          )}
        </li>
        : <li className={`depth1${(isActive ? ' is-active' : '')}`} onMouseEnter={() => {
          setIsActive(true)
        }} onMouseLeave={() => {
          setIsActive(false)
        }}>
          <a href="#" onClick={(e) => {
            e.preventDefault()
            setIsActive(!isActive)
          }}>{title}</a>
          {isActive && (
            <ul onClick={() => {
              setIsActive(!isActive)
            }}>
              {children}
            </ul>
          )}
        </li>
      }
    </>
  )
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接