You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

切换侧边栏时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>
  );
};

核心问题

  1. 如何阻止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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 22:42:02