首页 > 解决方案 > 解开 onMouseEnter、onMouseLeave 和 onClick 中的状态变化

问题描述

我有以下组件

  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>

在我的电脑上,当鼠标悬停在ListItemonMouseEnter激活和控制台日志上时

onMouseEnter
isActive true

在我的电脑上,当鼠标离开时ListItemonMouseLeave激活和控制台日志

onMouseLeave
isActive false

这里的一切都很好。当我打开导航菜单时,问题出在移动设备上,我控制台日志

isActive false

正如预期的那样。

但是当我ListItem第一次点击 a 时,我控制台日志

onMouseEnter
isActive true
onClick a
isActive false

而我的ListItem children不打开。但是当我ListItem第二次点击控制台日志时

onClick a
isActive true

这会打开我的ListItem children. 如果我ListItem第三次点击,我控制台日志

onClick a
isActive false

这关闭了我的ListItem children

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

标签: javascriptreactjs

解决方案


这是一个令人惊讶的棘手问题。您可以尝试几种方法。我没有时间亲自尝试它们。

  • 方法 1:您可以在元素上使用本机非冒泡事件(mouseenter、mouseleave)<li>。一个更方便地使用本机事件的钩子是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>
  • 方法 2:您可以检测是否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;
};

推荐阅读