useEffect(..., [props.scrollToIdx])) 要如何才能每次都执行?

在子组件内有:


    // 滚动
    useEffect(() => {
        if(props.scrollToIdx) {

            const scrollToIdx = props.scrollToIdx 
            
            console.log("滚动ID:", '#item-' + scrollToIdx!.toString())

            scrollTo(scrollToIdx)
        }
    }, [props.scrollToIdx])

    const scrollTo = (scrollToIdx: number) => {
        const container = document.querySelector("#container")
        const div: HTMLDivElement | null = document.querySelector('#item-' + scrollToIdx!.toString())
    
        console.log(container, div)
        if(container) {
            container.scrollTo({
                top: div!.offsetTop,
                behavior: 'smooth'
            });
        }
    }

在父组件

点击按钮时候会滑动:

  const btnClick = () => {
    console.log('点击按钮')
    setSelIdx(20)
  }

  return (
    <div className="App">
      
      <SubContainer {...props}></SubContainer>
    
      <button onClick={btnClick}>点击</button>
    </div>
  );

但是比如我在点击一次setSelIdx(20)之后,再点击,执行:
setSelIdx(20)

子组件就不会更新再继续执行这个代码了:

    useEffect(() => {
        if(props.scrollToIdx) {

            const scrollToIdx = props.scrollToIdx 
            
            console.log("滚动ID:", '#item-' + scrollToIdx!.toString())

            scrollTo(scrollToIdx)
        }
    }, [props.scrollToIdx])
也就是说:props.scrollToIdx 没有更新这里就不会执行,请问要如何保证父组件点击(btnClick ... setSelIdx(20))
子组件这里每次都执行?
阅读 1.8k
avatarAI BotBETA

为了确保 useEffect 钩子在 props.scrollToIdx 更改时总是执行,你需要确保 props.scrollToIdx 的值在每次点击时确实发生了变化。从你的描述来看,问题可能出在 props.scrollToIdx 的值在第二次点击时并没有真正改变(即它仍然是 20),因此 React 认为依赖项没有变化,因此不会重新运行 useEffect

要解决这个问题,你可以考虑以下几种方法:

  1. 确保 props.scrollToIdx 的值在每次点击时都不同
    如果 scrollToIdx 总是指向同一个值(如 20),则 useEffect 不会重新运行。你可以通过添加一个时间戳或递增的计数器来确保每次 scrollToIdx 都不同。
  2. 使用 useStateuseEffect 在子组件中控制滚动
    如果父组件仅仅是为了触发滚动,你可以考虑在子组件内部使用 useState 来控制滚动,而不是直接从 props 中获取。

方法 1: 更改 props.scrollToIdx 的值

在父组件中,你可以添加一个额外的状态来确保 scrollToIdx 每次都不同:

const [scrollCounter, setScrollCounter] = useState(0);

const btnClick = () => {
  console.log('点击按钮');
  setSelIdx(20);
  setScrollCounter(prev => prev + 1); // 更新计数器
}

// 传递 scrollCounter 或结合 selIdx 和 scrollCounter 作为新的 prop
return (
  <div className="App">
    <SubContainer scrollToIdx={[selIdx, scrollCounter]}></SubContainer>
    <button onClick={btnClick}>点击</button>
  </div>
);

然后在子组件中,你需要相应地调整 useEffect 依赖项和滚动逻辑。

方法 2: 在子组件中使用 useState

在子组件中,你可以使用 useState 来存储 scrollToIdx 的值,并在父组件触发时更新它:

// 子组件
const [localScrollToIdx, setLocalScrollToIdx] = useState(props.initialScrollToIdx || null);

useEffect(() => {
  if (props.scrollToIdx !== undefined) {
    setLocalScrollToIdx(props.scrollToIdx);
  }
}, [props.scrollToIdx]);

useEffect(() => {
  if (localScrollToIdx) {
    console.log("滚动ID:", '#item-' + localScrollToIdx.toString());
    scrollTo(localScrollToIdx);
  }
}, [localScrollToIdx]);

// ... 其余代码 ...

在父组件中,你只需传递初始的 scrollToIdx(如果需要的话),并在需要时更新它:

const [selIdx, setSelIdx] = useState(null);

const btnClick = () => {
  console.log('点击按钮');
  setSelIdx(20);
}

return (
  <div className="App">
    <SubContainer initialScrollToIdx={selIdx}></SubContainer>
    <button onClick={btnClick}>点击</button>
  </div>
);

这样,每次 selIdx 更新时,都会通过 props 传递给子组件,子组件的 useEffect 钩子将检测到 props.initialScrollToIdx 的变化(如果作为依赖项),但更推荐的做法是使用内部状态 localScrollToIdx 来控制滚动行为。

2 个回答
✓ 已被采纳

写 React 大家容易陷入一个误区:所有的功能都得通过 props 实现,props 一般用于响应式的更新。
例如你这个场景,如果 scrollToIdx 变化触发 scrollTo,scrollToIdx 不变不触发,使用 props 传递参数是合适的。
按照你的描述,是希望稳定触发 scrollTo,使用 props 去控制就不合适了,这个时候应该采用命令式调用,而不是响应式更新了。
可以使用 ref:

export interface SubContainerRef {
  scrollTo(index: number): void;
}

export const SubContainer = forwardRef<SubContainerRef, SubContainerProps>(
  (props, ref) => {
    const scrollTo = (scrollToIdx: number) => {
      const container = document.querySelector('#container');
      const div: HTMLDivElement | null = document.querySelector(
        '#item-' + scrollToIdx!.toString()
      );

      console.log(container, div);
      if (container) {
        container.scrollTo({
          top: div!.offsetTop,
          behavior: 'smooth',
        });
      }
    };

    useImperativeHandle(
      ref,
      () => ({
        scrollTo,
      }),
      []
    );
  //....
}

在父组件调用:

  const conRef = useRef<SubContainerRef>();

  const btnClick = () => {
    conRef.current?.scrollTo(20);
  };

  return (
    <div>
      <SubContainer ref={conRef} {...props} />
      <button onClick={btnClick}>点击</button>
    </div>
  );

总结一下:响应式更新使用 props,命令式调用使用 ref。

20 和 20 做比较的话是相等的,可以把它放置在一个对象里,这样可以触发 useEffect 的更新

import React from "react";
import ReactDOM from "react-dom";
import { useState, useEffect } from "react";

function SubContainer(props) {
  useEffect(() => {
    if (props.scrollToIdx) {
      console.log(props.scrollToIdx.ref);
    }
  }, [props.scrollToIdx]);
  return <div></div>;
}

function App() {
  const [scrollToIdx, SetScrollToIdx] = useState({ ref: null });
  const btnClick = () => {
    console.log("点击按钮");
    SetScrollToIdx((val) => {
      return {
        ...val,
        ref: 20
      };
    });
  };
  return (
    <div className="App">
      <SubContainer scrollToIdx={scrollToIdx}/>
      <button onClick={btnClick}>touch me</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Microsoft
子站问答
访问
宣传栏