迁移至Function/Hooks/Redux架构后react-select滚动同步失效问题
React类组件重构为函数组件后react-select滚动异常问题
问题背景
我正在重构应用,将顶层类组件改写为函数组件,同时使用useEffect Hooks和Redux,过程中遇到了滚动行为异常的问题。
- 旧版本(运行正常):https://stopthegop.netlify.app/
- 新版本(存在问题):https://61a2dbcce007ea00072b079c--stopthegop.netlify.app/
问题表现
我期望右侧div滚动时,左侧react-select组件的菜单列表同步滚动,保证右侧div顶部展示的主题始终在左侧菜单可见范围内。但实际情况为,滚动右侧div时,首次滚轮操作就会让左侧菜单自动回到顶部,后续还会触发3次重渲染,我手动触发滚动的代码完全不生效。我观察到正常运行的旧版本中左侧组件也会触发多次重渲染,但从未出现自动回到顶部的情况,我无法定位两个版本的差异原因,希望能得到相关解答,理解重渲染逻辑及修复方案。
初步排查
我初步排查后认为问题不来自顶层组件引用SearchBox组件的参数差异,两个版本的引用代码如下:
旧版本调用代码
<SearchBox groupedTopics={groupedTopics} topics={topics} ui={ui} vars={vars} inputValue={inputValue} stickyGroupHeadings={stickyGroupHeadings} searchRef={this.searchRef} filteringFunction={filteringFunction} isMenuOpen={isSearchMenuOpen(vars.IS_WIDE_SCREEN, selectionMade, isInitialHashValid, hashValue, hasFocus, anyTopicsForHash)} onFocus={this.handleSearchBoxFocus} onBlur={this.handleSearchBoxBlur} onClickGroupHeading={this.handleSearchBoxClickGroupHeading} onInputChange={this.handleSearchBoxInputChange} onSearchChange={this.handleSearchBoxSearchChange} onMenuScroll={this.handleSearchBoxMenuScroll} />
新版本调用代码
<SearchBox groupedTopics={groupedFilteredTopics} ui={ui} vars={vars} inputValue={inputValue} menuListRef={menuListRef} isMenuOpen={isSearchMenuOpen()} searchFilter={searchFilter} onClickGroupHeading={handleSearchBoxClickGroupHeading} onFocus={handleSearchBoxFocus} onBlur={handleSearchBoxBlur} onInputChange={handleSearchBoxInputChange} onScroll={handleSearchBoxScroll} onSelectTopic={handleSearchBoxSelectTopic} />
后续测试结论
测试发现旧版本中SearchBox是函数组件、Main是类组件时运行正常;将Main改为函数组件后问题出现,再把SearchBox改为类组件后问题又恢复正常。我原本是想通过改类组件使用shouldComponentUpdate方法做优化,但实际上不需要配置该方法仅改类组件就修复了问题。
目前测试结论:顶层组件和子组件仅有一个为函数组件时功能正常,二者同时为函数组件时就会出现滚动异常。我不认为当前是最终解决方案,函数组件本不该出现这类问题,请问这是什么原因导致的?是否是React本身的Bug?我希望能全量使用函数组件实现功能。
原因分析&修复方案
这不是React本身的Bug,是函数组件默认重渲染规则和react-select的菜单交互逻辑共同导致的问题:
- 类组件默认会对新老props做浅比较,props无变化就不会触发重渲染;但函数组件没有这个机制,只要父组件重渲染,子组件就会跟着重渲染,除非手动做缓存处理。
- react-select的菜单组件在重渲染时如果没有额外的滚动位置持久化逻辑,会默认把滚动条重置到顶部,你手动设置滚动的代码不生效,是因为重渲染发生在你滚动逻辑执行之后,新渲染的菜单DOM直接覆盖了你之前设置的滚动位置。
- 你可以任选以下方案实现全函数组件的需求:
- 用
React.memo包裹SearchBox函数组件,对传入的props做浅比较,避免不必要的重渲染。注意父组件传递的所有回调函数需要用useCallback包裹,引用类型的参数比如groupedFilteredTopics需要用useMemo包裹,保证重渲染时引用不变,否则React.memo不会生效。 - 如果确实需要SearchBox重渲染,你可以把菜单的滚动位置存在
useRef里,用useLayoutEffect在DOM更新完成后立刻把滚动位置赋值给菜单DOM,避免重渲染后位置被重置。 - 先检查
isSearchMenuOpen()的返回值是否在滚动时发生了不必要的切换,如果这个值频繁变动会导致react-select销毁重建菜单组件,滚动位置自然会被重置。
内容的提问来源于stack exchange,提问作者Dan Cancro
相关产品推荐
相关产品推荐

