React/Next.js/Redux应用中可复用数据加载组件的实现方案咨询
React/Next.js/Redux应用中可复用数据加载组件的实现方案咨询
嘿,我完全懂你的困扰——每个组件都要重复写一遍数据加载、状态判断的逻辑,时间久了不仅冗余还容易出错😣。咱们可以通过封装通用数据加载Hook或者通用数据加载组件来彻底消除这些重复代码,下面给你具体的思路和示例:
方案一:封装通用数据加载Hook
先把你现有的useSelectorEffect扩展成更通用的版本,把重复的请求触发、状态判断逻辑都抽离进去:
import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { StateLoading } from "@/shared/constants/loading"; // 定义Hook的参数类型 type DataLoaderOptions<T> = { dataSelector: (state: RootState) => T; statusSelector: (state: RootState) => StateLoading; fetchAction: () => void; dependencies?: React.DependencyList; }; export const useDataLoader = <T>({ dataSelector, statusSelector, fetchAction, dependencies = [], }: DataLoaderOptions<T>) => { const dispatch = useAppDispatch(); const data = useAppSelector(dataSelector); const status = useAppSelector(statusSelector); // 自动触发请求的逻辑:数据为空且非加载状态时发起请求 useEffect(() => { if (!data && status !== StateLoading.LOADING) { dispatch(fetchAction()); } }, [dispatch, data, status, fetchAction, ...dependencies]); // 返回统一的状态和数据 return { data, isLoading: status === StateLoading.LOADING, isClientLoaded: !!data, }; };
之后你的GamesDisplay组件就能简化成这样:
import { useDataLoader } from '@/hooks/useDataLoader'; import { getGames, selectGames, selectStatus } from "@/store/games/gamesSlice"; import { Game } from "@/shared/interfaces/game"; const GamesDisplay = () => { const { data: games, isLoading, isClientLoaded } = useDataLoader<Game[]>({ dataSelector: selectGames, statusSelector: selectStatus, fetchAction: getGames, }); if (isLoading) { return <div>Loading....</div>; } return ( <> <div>Games Display</div> {isClientLoaded && ( <ul> {games?.map((game) => ( <li key={game.id}>{game.name}</li> ))} </ul> )} </> ); };
方案二:封装通用数据加载组件
如果连加载状态的UI(比如Loading提示、空数据提示)都重复,还可以把UI和数据逻辑一起封装成通用组件:
import { ReactNode } from 'react'; import { useDataLoader } from '@/hooks/useDataLoader'; import { StateLoading } from "@/shared/constants/loading"; type DataLoaderProps<T> = { dataSelector: (state: RootState) => T; statusSelector: (state: RootState) => StateLoading; fetchAction: () => void; dependencies?: React.DependencyList; loadingUI?: ReactNode; // 自定义加载UI emptyUI?: ReactNode; // 自定义空数据UI children: (data: T, isClientLoaded: boolean) => ReactNode; }; export const DataLoader = <T>({ dataSelector, statusSelector, fetchAction, dependencies = [], loadingUI = <div>Loading....</div>, emptyUI = <div>No data available</div>, children, }: DataLoaderProps<T>) => { const { data, isLoading, isClientLoaded } = useDataLoader({ dataSelector, statusSelector, fetchAction, dependencies, }); if (isLoading) { return loadingUI; } if (!isClientLoaded) { return emptyUI; } return children(data, isClientLoaded); };
用这个组件渲染GamesDisplay会更简洁:
import { DataLoader } from '@/components/DataLoader'; import { getGames, selectGames, selectStatus } from "@/store/games/gamesSlice"; import { Game } from "@/shared/interfaces/game"; const GamesDisplay = () => { return ( <DataLoader<Game[]> dataSelector={selectGames} statusSelector={selectStatus} fetchAction={getGames} > {(games) => ( <> <div>Games Display</div> <ul> {games?.map((game) => ( <li key={game.id}>{game.name}</li> ))} </ul> </> )} </DataLoader> ); };
这样不管是后续的用户列表、订单列表组件,都能直接复用这两个封装后的工具,再也不用重复写相同的加载逻辑啦!
备注:内容来源于stack exchange,提问作者RasMason
相关产品推荐
相关产品推荐

