自定义数据获取Hook优化咨询:Ant Design表格实现疑问
Hey there! Let's break down your questions and walk through some practical optimizations for your custom Hook. I totally get loving Ant Design Tables—they're such a reliable tool for building data grids quickly!
Your Core Questions, Answered
1. Loading State & Fetcher Execution
Your current approach to dispatching the FETCHING state before calling the fetcher is totally valid—it gives immediate feedback to users that data is loading, which is great for UX. That said, we can make this more robust:
- Add error handling: Right now, if the fetcher fails, your state gets stuck in
loading: trueforever. We should catch errors and update the state to reflect that, so you can show error messages to users. - Cancel stale requests: When the component unmounts or parameters change, you should cancel the ongoing request to avoid outdated data from overwriting fresh state.
AbortControlleris perfect for this.
2. Reducer Inside the Hook Body
It makes sense that you put the reducer inside the Hook to access history and location, but this couples the reducer to routing logic unnecessarily. A cleaner approach is to:
- Move the reducer outside the Hook (so it's pure and reusable)
- Handle pagination routing logic separately in the Hook, then merge the
onChangehandler into the pagination state after the reducer updates it
Optimized Code with Explanations
Here's a revised version of your Hook with these improvements, plus some extra tweaks for maintainability:
import { useEffect, useReducer, useCallback } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import queryString from 'query-string'; // Pure reducer: no external dependencies, just state + actions const initialState = { dataSource: [], loading: true, error: null, // Track errors for user feedback pagination: {} }; const reducer = (state, action) => { switch (action.type) { case 'FETCHING': // Keep existing data/pagination to avoid UI jumps during loading return { ...state, loading: true, error: null }; case 'DATA_RECEIVED': return { ...state, loading: false, dataSource: action.payload.content, pagination: { pageSize: action.payload.pageable.pageSize, current: action.payload.pageable.pageNumber + 1, total: action.payload.totalElements } }; case 'FETCH_FAILED': return { ...state, loading: false, error: action.payload }; default: throw new Error(`Unknown action type: ${action.type}`); } }; const useGridDataFetch = (fetcher, initialParams = {}) => { const location = useLocation(); const history = useHistory(); const params = { ...initialParams, ...queryString.parse(location.search) }; // Memoize page change handler to avoid unnecessary re-renders const handlePageChange = useCallback((page) => { const currentQuery = queryString.parse(location.search); // Use query-string's stringify to cleanly update URL params const newQuery = queryString.stringify({ ...currentQuery, page }); history.push(`${history.location.pathname}?${newQuery}`); }, [location.search, history]); const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { const abortController = new AbortController(); const signal = abortController.signal; const fetchData = async () => { dispatch({ type: 'FETCHING' }); try { // Pass abort signal to fetcher (adjust if your fetcher uses axios cancel tokens instead) const response = await fetcher({ ...params, page: params.page > 0 ? params.page - 1 : params.page, signal }); dispatch({ type: 'DATA_RECEIVED', payload: response.data }); } catch (error) { // Ignore abort errors (they're intentional when components unmount) if (error.name !== 'AbortError') { dispatch({ type: 'FETCH_FAILED', payload: error.message }); } } }; fetchData(); // Cleanup: cancel request when component unmounts or params change return () => abortController.abort(); }, [location.search, fetcher, params]); // Merge pagination onChange handler separately (keeps reducer pure) const enhancedPagination = { ...state.pagination, onChange: handlePageChange }; // Memoized refresh function for manual data reloads const refresh = useCallback((extraParams = {}) => { const abortController = new AbortController(); const signal = abortController.signal; const fetchFreshData = async () => { dispatch({ type: 'FETCHING' }); try { const mergedParams = { ...params, ...extraParams }; const response = await fetcher({ ...mergedParams, page: mergedParams.page > 0 ? mergedParams.page - 1 : mergedParams.page, signal }); dispatch({ type: 'DATA_RECEIVED', payload: response.data }); } catch (error) { if (error.name !== 'AbortError') { dispatch({ type: 'FETCH_FAILED', payload: error.message }); } } }; fetchFreshData(); return () => abortController.abort(); }, [params, fetcher]); return [{ ...state, pagination: enhancedPagination }, refresh]; }; export default useGridDataFetch;
Key Improvements
- Error resilience: The
errorstate lets you display friendly error messages instead of leaving users stuck on loading. - Request cancellation:
AbortControllerprevents outdated API responses from messing with your state after the component unmounts or params change. - Pure reducer: Moving the reducer outside the Hook makes it easier to test and reuse, since it no longer depends on routing hooks.
- Cleaner URL handling:
queryString.stringifyreplaces manual string concatenation, avoiding bugs with special characters or missing parameters. - Memoized functions:
useCallbackonhandlePageChangeandrefreshreduces unnecessary re-renders in child components.
内容的提问来源于stack exchange,提问作者claud.io

