切换侧边栏时ActionsList组件重置问题求助
问题描述
切换左侧侧边栏时,ActionsList组件会重新渲染,导致已加载的操作状态丢失,如何阻止这种情况发生?
当前实现代码
const ApiExplorer = ({ openapi, globalUrl }) => { const [actions, setActions] = useState([]); const [info, setInfo] = useState({}); const [serverurl, setServerUrl] = useState(""); const [ExampleBody, setExampleBody] = useState(actions[0]?.body); const [apiResponse, setApiResponse] = useState({}); const [filteredActions, setFilteredActions] = useState([]); const [isLoading, setIsLoading] = useState(false); const loadAction = 2; const loadedAction = useRef(null); const Action = React.memo(()=>{ // action code }) const ActionsList = memo(() => { const [searchQuery, setSearchQuery] = useState(""); const [actionsList, setActionsList] = useState([]); const [selectedActionIndex, setSelectedActionIndex] = useState(0); const [hasMore, setHasMore] = useState(true); // New state to track if there's more to load const itemPerLoad = 10; // Number of items to load per scroll const containerRef = useRef(null); // Initial loading and search filtering useLayoutEffect(() => { if (searchQuery === "") { setActionsList(actions.slice(0, itemPerLoad)); } else { const filtered = actions.filter(action => action.name.toLowerCase().includes(searchQuery.toLowerCase()) ); setActionsList(filtered.slice(0, itemPerLoad)); } }, [searchQuery]); // Load more items when scrolling down const loadMoreActions = () => { setActionsList((prevActions) => { const nextActions = actions.slice(prevActions.length, prevActions.length + itemPerLoad); if (nextActions.length < itemPerLoad) { setHasMore(false); // Stop loading if there are no more items } return [...prevActions, ...nextActions]; }); }; const handleScroll = () => { const container = containerRef.current; if (container) { const scrollThreshold = 10; const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - scrollThreshold; if (isAtBottom && hasMore) { loadMoreActions(); } } }; useLayoutEffect(() => { const container = containerRef.current; if (container) { container.addEventListener("scroll", handleScroll); } return () => { if (container) { container.removeEventListener("scroll", handleScroll); } }; }, [hasMore]); const handleActionClick = (index, action) => { setSelectedActionIndex(index); setExampleBody(action.example_response); }; return ( <div style={{ width: 350, overflow: 'hidden' }}> <TextField value={searchQuery} placeholder="Search here" onChange={(e) => setSearchQuery(e.target.value)} InputProps={{ startAdornment: ( <InputAdornment position="start"> <SearchIcon /> </InputAdornment> ), style: { height: 51 }, }} sx={{ marginLeft: 2, "& .MuiOutlinedInput-root fieldset": { border: "1px solid rgba(73, 73, 73, 1)", }, }} /> <div ref={containerRef} style={{ marginLeft: 20, marginTop: 10, backgroundColor: '#1a1a1a', overflowY: 'auto', height: 'calc(100vh - 150px)', }} > {actionsList.length > 0 ? ( actionsList.map((action, actionIndex) => ( <Button key={action.name} sx={{ padding: "15px 5px", textTransform: "none", backgroundColor: selectedActionIndex === actionIndex ? "#3f3f3f" : "transparent", justifyContent: "flex-start", marginBottom: "5px", width: "100%", display: "flex", "&:hover": { backgroundColor: "#2f2f2f", }, }} onClick={() => handleActionClick(actionIndex, action)} > <span style={{ color: RequestMethods.find((method) => method.value === action.method)?.color, fontWeight: "bold", minWidth: 70, textAlign: "start", }} > {action.method} </span> <span style={{ color: "white", textOverflow: "ellipsis", overflow: "hidden", }} > {action.name.replaceAll("_", " ")} </span> </Button> )) ) : ( <div style={{ padding: "15px", color: "white", textAlign: "center" }}> No actions found </div> )} </div> </div> ); },[]); return ( <div style={{ display: "flex", overflow: 'hidden', height: "calc(100vh - 70px)", }} > <ActionsList /> <div ref={loadedAction} style={{ width: "100%", height: '80vh', overflowY:'auto', marginLeft: 10, alignItems: "center" }}> <div style={{ width: "100%", maxWidth: 1920, margin: 0, padding: 0}}> <h1> {info?.title ? ( <div style={{ display: "flex", alignItems: "center" }}> <img src={openapi?.info["x-logo"]} width={50} height={50} alt="app logo" style={{ marginLeft: 20, borderRadius: 8 }} /> <Typography style={{ fontSize: 24, fontWeight: 700, marginLeft: 20 }}> {info.title} </Typography> </div> ) : ( "API Explorer" )} </h1> <div style={{ paddingTop: 20,}}> {filteredActions.map((action, index) => ( <Action key={index} action={action} index={index} serverurl={serverurl} setApiResponse={setApiResponse} setExampleBody={setExampleBody} /> ))} </div> </div> <ActionResponse apiResponse={apiResponse} ExampleBody={ExampleBody} /> </div> </div> ); };
核心问题
- 如何阻止
ActionsList在切换侧边栏时重置状态?
已尝试方案
- 使用
useMemo和useCallback阻止重渲染,但未生效; - 将搜索栏与操作列表拆分为独立组件,问题仍存在;
- 将
ActionsList逻辑移至上层组件,未解决问题。
注:每次切换侧边栏时,ActionsList会重渲染两次。
解决方案
1. 核心原因分析
你的ActionsList组件定义在ApiExplorer内部,每次ApiExplorer重渲染(比如切换侧边栏触发父组件状态变化),都会重新创建一个新的ActionsList组件类型。此时即使加了memo,也无法阻止组件重新挂载——因为React会认为这是一个全新的组件,自然会重置内部所有状态。
另外,ActionsList内部使用了父组件的actions和setExampleBody,但memo的依赖数组是空的,导致内部使用的actions是旧的快照,同时父组件传递的隐式依赖变化时,组件无法正确响应。
2. 具体修改步骤
步骤1:将ActionsList移到父组件外部
把ActionsList从ApiExplorer内部抽出来,作为独立组件,通过props接收需要的依赖(比如actions、setExampleBody),这样组件引用会保持稳定,memo才能生效。
// 移到ApiExplorer外部 const ActionsList = memo(({ actions, setExampleBody }) => { const [searchQuery, setSearchQuery] = useState(""); const [actionsList, setActionsList] = useState([]); const [selectedActionIndex, setSelectedActionIndex] = useState(0); const [hasMore, setHasMore] = useState(true); const itemPerLoad = 10; const containerRef = useRef(null); // 修复依赖:加上actions,确保actions更新时同步列表 useLayoutEffect(() => { if (searchQuery === "") { setActionsList(actions.slice(0, itemPerLoad)); } else { const filtered = actions.filter(action => action.name.toLowerCase().includes(searchQuery.toLowerCase()) ); setActionsList(filtered.slice(0, itemPerLoad)); } // 每次actions变化时重置加载状态 setHasMore(actions.length > itemPerLoad); }, [searchQuery, actions]); // 用useCallback包裹函数,避免引用变化触发不必要重渲染 const loadMoreActions = useCallback(() => { setActionsList((prevActions) => { const nextActions = actions.slice(prevActions.length, prevActions.length + itemPerLoad); if (nextActions.length < itemPerLoad) { setHasMore(false); } return [...prevActions, ...nextActions]; }); }, [actions, itemPerLoad, setHasMore]); const handleScroll = useCallback(() => { const container = containerRef.current; if (container) { const scrollThreshold = 10; const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - scrollThreshold; if (isAtBottom && hasMore) { loadMoreActions(); } } }, [hasMore, loadMoreActions]); useLayoutEffect(() => { const container = containerRef.current; if (container) { container.addEventListener("scroll", handleScroll); } return () => { if (container) { container.removeEventListener("scroll", handleScroll); } }; }, [handleScroll]); // 依赖改为handleScroll const handleActionClick = (index, action) => { setSelectedActionIndex(index); setExampleBody(action.example_response); }; // ... 原return部分代码不变 }, (prevProps, nextProps) => { // 自定义memo比较逻辑:只有actions引用变化时才重渲染 return prevProps.actions === nextProps.actions; });
步骤2:在父组件中传递props
修改ApiExplorer中ActionsList的调用,传递所需的props:
// 在ApiExplorer的return中 return ( <div style={{ display: "flex", overflow: 'hidden', height: "calc(100vh - 70px)" }}> <ActionsList actions={actions} setExampleBody={setExampleBody} /> {/* 其他代码不变 */} </div> );
步骤3:优化Action组件(可选)
同样,Action组件也定义在父组件内部,建议移到外部并通过props传递依赖,避免同样的重渲染问题:
// 移到ApiExplorer外部 const Action = memo(({ action, index, serverurl, setApiResponse, setExampleBody }) => { // 原action代码 }, (prevProps, nextProps) => { // 根据需要自定义比较逻辑,比如action引用不变时不重渲染 return prevProps.action === nextProps.action && prevProps.serverurl === nextProps.serverurl; });
3. 为什么之前的尝试无效?
- 你之前用
memo但没解决组件重新创建的问题:因为组件定义在父组件内部,每次父组件重渲染都会生成新的组件类型,React会销毁旧组件并挂载新组件,状态自然重置。 useMemo/useCallback没生效:因为组件本身被重新创建了,这些优化无法阻止组件重新挂载。
内容的提问来源于stack exchange,提问作者Lalit deore
相关产品推荐
相关产品推荐

