React中将数据查询代码拆分为接口+实现类的方案是否合理?
问题解答
A、该方案在React中是否是好的设计?
你的核心设计思路非常合理:将视图层和数据访问层分离、通过接口抽象DAO实现的思路,不管是后端Java还是前端React都是通用的优秀设计,能够有效降低代码耦合度,后续更换数据库实现时确实不需要改动上层视图逻辑,这个设计方向完全没有问题。
你当前示例代码存在一些不符合React最佳实践的小问题,但不属于设计思路的错误,调整实现细节即可。
B、这类设计需要注意哪些问题?
你当前的代码和这类架构落地时,需要重点关注以下几点:
- 示例中的
useEffect没有声明依赖数组,会导致组件每次渲染都重复发起getThing请求,严重浪费性能,需要根据需求补充依赖数组:如果只需要组件挂载时请求一次,补充空数组[]作为useEffect的第二个参数即可。 - 异步请求存在组件卸载后更新状态的风险:如果
getThing请求还没返回时组件就被卸载,后续回调执行setThing会触发React的状态更新警告,极端情况下会引发内存泄漏,需要在useEffect的清理函数中标记组件卸载状态、或者取消请求。 - 全局单例DAO的适用边界:当前示例中DAO是全局初始化的单例,如果你的DAO实现类存在可变的内部状态,多个组件调用时会出现状态污染,如果你有服务端渲染(SSR)需求,全局单例还会出现跨请求的数据串扰问题,这种场景下需要将DAO实例的生命周期和组件/请求绑定。
- 订阅类逻辑的清理:如果你的DAO实现中包含firebase实时监听、websocket推送这类长连接订阅逻辑,必须在
useEffect的清理函数中调用对应的取消订阅方法,否则会产生明确的内存泄漏。
下面是修正了上述问题的示例代码:
import { useState, useEffect, useContext } from 'react'; interface Dao { getThing: (id: string) => Promise<string> } class DaoSpecificImpl implements Dao { tableName: string; constructor(tableName: string) { this.tableName = tableName; } getThing = async (id: string) => { // 调用firebase等特定数据库 // 从表名为tableName的表中获取数据 return "herp"; } } const Display: React.FC = () => { const [thing, setThing] = useState("derp"); const dao = new DaoSpecificImpl("thingies"); useEffect(() => { let isUnmounted = false; dao.getThing("123").then((newThing) => { if (!isUnmounted) { setThing(newThing); } }); // 组件卸载时执行的清理逻辑 return () => { isUnmounted = true; }; }, [dao]); // 声明依赖,dao实例变化时才会重新请求 return ( <div>{thing}</div> ) } export default Display;
C、React生态有没有更适配这类场景的最优模式?
有几个React生态常用的模式,可以让你的架构更灵活、更符合前端开发习惯:
- 基于React Context的依赖注入:不要直接在组件中引用全局DAO实例,而是通过Context将DAO实例注入到组件树中,后续切换实现、单元测试打桩都只需要修改Context Provider的传值,不需要改动任何上层组件代码,示例如下:
// 1、创建DAO Context const DaoContext = createContext<Dao>(new DaoSpecificImpl("default_table")); // 2、根组件挂载时提供实际的DAO实现 function App() { // 后续切换实现只需要改这里的实例即可 const daoImpl = new DaoSpecificImpl("thingies"); return ( <DaoContext.Provider value={daoImpl}> {/* 所有子组件都可以拿到DAO实例 */} <Display /> </DaoContext.Provider> ) } // 3、组件中使用DAO const Display: React.FC = () => { const dao = useContext(DaoContext); // 后续逻辑不变 }
- 自定义Hook封装数据逻辑:可以把DAO调用、状态管理、错误处理、加载状态都封装到自定义Hook中,视图层只需要调用Hook拿到最终数据和状态即可,更符合React函数式编程的习惯,示例:
function useThing(id: string) { const dao = useContext(DaoContext); const [data, setData] = useState<string | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); useEffect(() => { let isUnmounted = false; setLoading(true); dao.getThing(id) .then(res => !isUnmounted && setData(res)) .catch(err => !isUnmounted && setError(err)) .finally(() => !isUnmounted && setLoading(false)); return () => { isUnmounted = true }; }, [id, dao]); return { data, loading, error }; } // 组件中使用非常简洁 const Display: React.FC = () => { const { data: thing, loading, error } = useThing("123"); if (loading) return <div>加载中</div>; if (error) return <div>加载失败</div>; return <div>{thing}</div> }
- 搭配成熟的请求库使用:如果你不想自己处理缓存、请求去重、重试、自动取消这类逻辑,可以直接用TanStack Query(原React Query)、SWR这类生态成熟的请求库,只需要把你的DAO方法作为请求函数传给这些库即可,能节省大量重复的模板代码。
内容的提问来源于stack exchange,提问作者Creature
相关产品推荐
相关产品推荐

