【问题标题】:Untangling state changes in onMouseEnter, onMouseLeave, and onClick解开 onMouseEnter、onMouseLeave 和 onClick 中的状态变化
【发布时间】:2021-03-24 15:18:45
【问题描述】:

我有以下组件

  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>

在我的 PC 上,当鼠标悬停在 ListItem 上时,onMouseEnter 激活并控制台日志

onMouseEnter
isActive true

在我的电脑上,当鼠标离开ListItem,onMouseLeave 激活并控制台日志

onMouseLeave
isActive false

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

isActive false

正如预期的那样。

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

onMouseEnter
isActive true
onClick a
isActive false

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

onClick a
isActive true

打开我的ListItemchildren。如果我第三次点击ListItem,我会控制台日志

onClick a
isActive false

关闭我的ListItem children

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

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    这是一个非常棘手的问题。您可以尝试几种方法。我还没有时间亲自尝试。

    • 方法 1: 您可以在&lt;li&gt; 元素上使用本机非冒泡事件(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>
    
    • 方法 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;
    };
    
    • 方法 3: 您可以启动一个计时器,例如每次切换后 250 毫秒并忽略进一步切换,直到计时器到期

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

    【讨论】:

      【解决方案2】:

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

      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>
            }
          </>
        )
      }
      

      【讨论】:

        猜你喜欢
        • 2021-06-20
        • 1970-01-01
        • 2013-07-06
        • 1970-01-01
        • 2017-12-28
        • 2020-11-23
        • 2019-04-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多