♻️ refactor(StyleContext): modernize context architecture and eliminate route transition flicker

## Breaking Changes
- Remove backward compatibility layer for old action types
- StyleContext is no longer exported, use useStyle hook instead

## Major Improvements
- **Architecture**: Replace useState with useReducer for complex state management
- **Performance**: Add debounced resize handling and batch updates via BATCH_UPDATE action
- **DX**: Export useStyle hook and styleActions for type-safe usage
- **Memory**: Use useMemo to cache context value and prevent unnecessary re-renders

## Bug Fixes
- **UI**: Eliminate padding flicker when navigating to /console/chat* and /console/playground routes
- **Logic**: Remove redundant localStorage operations and state synchronization

## Implementation Details
- Define ACTION_TYPES and ROUTE_PATTERNS constants for better maintainability
- Add comprehensive JSDoc documentation for all functions
- Extract custom hooks: useWindowResize, useRouteChange, useMobileSiderAutoHide
- Calculate shouldInnerPadding directly in PageLayout based on pathname to prevent async updates
- Integrate localStorage saving logic into SET_SIDER_COLLAPSED reducer case
- Remove SET_INNER_PADDING action as it's no longer needed

## Updated Components
- PageLayout.js: Direct padding calculation based on route
- HeaderBar.js: Use new useStyle hook and styleActions
- SiderBar.js: Remove redundant localStorage calls
- LogsTable.js: Remove unused StyleContext import
- Playground/index.js: Migrate to new API

## Performance Impact
- Reduced component re-renders through optimized context structure
- Eliminated unnecessary effect dependencies and state updates
- Improved route transition smoothness with synchronous padding calculation
This commit is contained in:
Apple\Apple
2025-06-02 04:16:48 +08:00
parent 90d4e0e41c
commit cc3f3cf033
15 changed files with 261 additions and 190 deletions

View File

@@ -1,117 +1,227 @@
// contexts/User/index.jsx
// contexts/Style/index.js
import React, { useState, useEffect, useCallback } from 'react';
import React, { useReducer, useEffect, useMemo, createContext } from 'react';
import { useLocation } from 'react-router-dom';
import { isMobile as getIsMobile } from '../../helpers/index.js';
export const StyleContext = React.createContext({
dispatch: () => null,
});
// Action Types
const ACTION_TYPES = {
TOGGLE_SIDER: 'TOGGLE_SIDER',
SET_SIDER: 'SET_SIDER',
SET_MOBILE: 'SET_MOBILE',
SET_SIDER_COLLAPSED: 'SET_SIDER_COLLAPSED',
BATCH_UPDATE: 'BATCH_UPDATE',
};
// Constants
const STORAGE_KEYS = {
SIDEBAR_COLLAPSED: 'default_collapse_sidebar',
};
const ROUTE_PATTERNS = {
CONSOLE: '/console',
};
/**
* 判断路径是否为控制台路由
* @param {string} pathname - 路由路径
* @returns {boolean} 是否为控制台路由
*/
const isConsoleRoute = (pathname) => {
return pathname === ROUTE_PATTERNS.CONSOLE ||
pathname.startsWith(ROUTE_PATTERNS.CONSOLE + '/');
};
/**
* 获取初始状态
* @param {string} pathname - 当前路由路径
* @returns {Object} 初始状态对象
*/
const getInitialState = (pathname) => {
const isMobile = getIsMobile();
const isConsole = isConsoleRoute(pathname);
const isCollapsed = localStorage.getItem(STORAGE_KEYS.SIDEBAR_COLLAPSED) === 'true';
return {
isMobile,
showSider: isConsole && !isMobile,
siderCollapsed: isCollapsed,
isManualSiderControl: false,
};
};
/**
* Style reducer
* @param {Object} state - 当前状态
* @param {Object} action - action 对象
* @returns {Object} 新状态
*/
const styleReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPES.TOGGLE_SIDER:
return {
...state,
showSider: !state.showSider,
isManualSiderControl: true,
};
case ACTION_TYPES.SET_SIDER:
return {
...state,
showSider: action.payload,
isManualSiderControl: action.isManualControl ?? false,
};
case ACTION_TYPES.SET_MOBILE:
return {
...state,
isMobile: action.payload,
};
case ACTION_TYPES.SET_SIDER_COLLAPSED:
// 自动保存到 localStorage
localStorage.setItem(STORAGE_KEYS.SIDEBAR_COLLAPSED, action.payload.toString());
return {
...state,
siderCollapsed: action.payload,
};
case ACTION_TYPES.BATCH_UPDATE:
return {
...state,
...action.payload,
};
default:
return state;
}
};
// Context (内部使用,不导出)
const StyleContext = createContext(null);
/**
* 自定义 Hook - 处理窗口大小变化
* @param {Function} dispatch - dispatch 函数
* @param {Object} state - 当前状态
* @param {string} pathname - 当前路径
*/
const useWindowResize = (dispatch, state, pathname) => {
useEffect(() => {
const handleResize = () => {
const isMobile = getIsMobile();
dispatch({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile });
// 只有在非手动控制的情况下,才根据屏幕大小自动调整侧边栏
if (!state.isManualSiderControl && isConsoleRoute(pathname)) {
dispatch({
type: ACTION_TYPES.SET_SIDER,
payload: !isMobile,
isManualControl: false
});
}
};
let timeoutId;
const debouncedResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(handleResize, 150);
};
window.addEventListener('resize', debouncedResize);
return () => {
window.removeEventListener('resize', debouncedResize);
clearTimeout(timeoutId);
};
}, [dispatch, state.isManualSiderControl, pathname]);
};
/**
* 自定义 Hook - 处理路由变化
* @param {Function} dispatch - dispatch 函数
* @param {string} pathname - 当前路径
*/
const useRouteChange = (dispatch, pathname) => {
useEffect(() => {
const isMobile = getIsMobile();
const isConsole = isConsoleRoute(pathname);
dispatch({
type: ACTION_TYPES.BATCH_UPDATE,
payload: {
showSider: isConsole && !isMobile,
isManualSiderControl: false,
},
});
}, [pathname, dispatch]);
};
/**
* 自定义 Hook - 处理移动设备侧边栏自动收起
* @param {Object} state - 当前状态
* @param {Function} dispatch - dispatch 函数
*/
const useMobileSiderAutoHide = (state, dispatch) => {
useEffect(() => {
// 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
if (state.isMobile && state.showSider && !state.isManualSiderControl) {
dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
}
}, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
};
/**
* Style Provider 组件
*/
export const StyleProvider = ({ children }) => {
const location = useLocation();
const initialIsMobile = getIsMobile();
const pathname = location.pathname;
const initialPathname = location.pathname;
let initialShowSiderValue = false;
let initialInnerPaddingValue = false;
const [state, dispatch] = useReducer(
styleReducer,
pathname,
getInitialState
);
if (initialPathname.includes('/console')) {
initialShowSiderValue = !initialIsMobile;
initialInnerPaddingValue = true;
}
useWindowResize(dispatch, state, pathname);
useRouteChange(dispatch, pathname);
useMobileSiderAutoHide(state, dispatch);
const [state, setState] = useState({
isMobile: initialIsMobile,
showSider: initialShowSiderValue,
siderCollapsed: false,
shouldInnerPadding: initialInnerPaddingValue,
manualSiderControl: false,
});
const dispatch = useCallback((action) => {
if ('type' in action) {
switch (action.type) {
case 'TOGGLE_SIDER':
setState((prev) => ({
...prev,
showSider: !prev.showSider,
manualSiderControl: true
}));
break;
case 'SET_SIDER':
setState((prev) => ({
...prev,
showSider: action.payload,
manualSiderControl: action.manual || false
}));
break;
case 'SET_MOBILE':
setState((prev) => ({ ...prev, isMobile: action.payload }));
break;
case 'SET_SIDER_COLLAPSED':
setState((prev) => ({ ...prev, siderCollapsed: action.payload }));
break;
case 'SET_INNER_PADDING':
setState((prev) => ({ ...prev, shouldInnerPadding: action.payload }));
break;
default:
setState((prev) => ({ ...prev, ...action }));
}
} else {
setState((prev) => ({ ...prev, ...action }));
}
}, []);
useEffect(() => {
const updateMobileStatus = () => {
const currentIsMobile = getIsMobile();
if (!currentIsMobile &&
(location.pathname === '/console' || location.pathname.startsWith('/console/'))) {
dispatch({ type: 'SET_SIDER', payload: true, manual: false });
}
dispatch({ type: 'SET_MOBILE', payload: currentIsMobile });
};
window.addEventListener('resize', updateMobileStatus);
return () => window.removeEventListener('resize', updateMobileStatus);
}, [dispatch, location.pathname]);
useEffect(() => {
if (state.isMobile && state.showSider && !state.manualSiderControl) {
dispatch({ type: 'SET_SIDER', payload: false });
}
}, [state.isMobile, state.showSider, state.manualSiderControl, dispatch]);
useEffect(() => {
const currentPathname = location.pathname;
const currentlyMobile = getIsMobile();
if (currentPathname === '/console' || currentPathname.startsWith('/console/')) {
dispatch({
type: 'SET_SIDER',
payload: !currentlyMobile,
manual: false
});
dispatch({ type: 'SET_INNER_PADDING', payload: true });
} else {
dispatch({
type: 'SET_SIDER',
payload: false,
manual: false
});
dispatch({ type: 'SET_INNER_PADDING', payload: false });
}
}, [location.pathname, dispatch]);
useEffect(() => {
const isCollapsed =
localStorage.getItem('default_collapse_sidebar') === 'true';
dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
}, [dispatch]);
const contextValue = useMemo(
() => ({ state, dispatch }),
[state]
);
return (
<StyleContext.Provider value={[state, dispatch]}>
<StyleContext.Provider value={contextValue}>
{children}
</StyleContext.Provider>
);
};
/**
* 自定义 Hook - 使用 StyleContext
* @returns {{state: Object, dispatch: Function}} context value
*/
export const useStyle = () => {
const context = React.useContext(StyleContext);
if (!context) {
throw new Error('useStyle must be used within StyleProvider');
}
return context;
};
// 导出 action creators 以便外部使用
export const styleActions = {
toggleSider: () => ({ type: ACTION_TYPES.TOGGLE_SIDER }),
setSider: (show, isManualControl = false) => ({
type: ACTION_TYPES.SET_SIDER,
payload: show,
isManualControl
}),
setMobile: (isMobile) => ({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile }),
setSiderCollapsed: (collapsed) => ({
type: ACTION_TYPES.SET_SIDER_COLLAPSED,
payload: collapsed
}),
};