React中获取大型可编辑数据的合理流程探究
一、何时获取数据
针对不能使用useEffect、无法SSR且需解耦导航与数据获取的约束,推荐两种落地方案:
1. 路由层面前置加载(React Router v6.4+)
如果项目使用React Router,可通过路由loader函数在页面渲染前完成数据获取,完全解耦组件渲染与数据逻辑,天然支持页面刷新场景:
// 路由配置 const router = createBrowserRouter([ { path: '/editor/:id', element: <EditorPage />, loader: async ({ params }) => { const data = await fetchData(params.id); return { data }; }, }, ]); // EditorPage组件 const EditorPage = () => { const { data } = useLoaderData(); return <Editor initialData={data} />; };
路由loader会在组件渲染前执行,数据就绪后才渲染页面,既规避了useEffect的异步竞态问题,也不会和导航逻辑绑定。
2. React 18 use 钩子直接获取
若不依赖路由加载器,React 18的use钩子允许在组件体中直接调用异步函数,替代useEffect实现初始数据获取:
const EditorPage = () => { const data = use(fetchData()); if (!data) { return <div>加载中...</div>; // 展示加载状态,避免空白 } return <Editor initialData={data} />; };
use钩子会自动处理异步状态,数据获取完成后组件自动重渲染,无需手动维护状态,也避开了useEffect的反模式争议。
二、存储数据的位置
针对避免数据重复存储、支持多Editor实例的需求,推荐以下两种方案:
1. 页面层作为唯一数据源,Editor仅做视图代理
让Page组件持有完整数据,Editor不维护独立的完整状态,仅通过回调向Page层提交修改,由Page层统一处理更新:
// Page组件 import { produce } from 'immer'; const EditorPage = () => { const data = use(fetchData()); const [currentData, setCurrentData] = useState(data); const handleDataChange = (updates) => { // 用Immer优化大型数据的不可变更新,减少内存开销 setCurrentData(prev => produce(prev, draft => { Object.assign(draft, updates); })); }; if (!data) return <div>加载中...</div>; return <Editor data={currentData} onChange={handleDataChange} />; }; // Editor组件 const Editor = ({ data, onChange }) => { // 仅维护表单控件的临时输入状态,提交时触发源数据更新 const handleInputChange = (field, value) => { onChange({ [field]: value }); }; return ( <form> <input value={data.title} onChange={(e) => handleInputChange('title', e.target.value)} /> {/* 其他表单字段 */} </form> ); };
这种模式下仅Page层持有完整数据,Editor仅做视图渲染和修改触发,彻底避免数据重复存储问题,同时Immer能大幅降低大型数据更新的性能损耗。
2. Redux实例化切片(支持多Editor实例隔离)
如果需要在多个组件间共享单个Editor的数据但不污染全局状态,可以为每个Editor实例创建独立的Redux切片,通过唯一标识区分:
import { createSlice } from '@reduxjs/toolkit'; import { produce } from 'immer'; // 动态创建Editor实例专属切片 const createEditorSlice = (instanceId) => { return createSlice({ name: `editor/${instanceId}`, initialState: null, reducers: { setData: (state, action) => action.payload, updateData: (state, action) => produce(state, draft => { Object.assign(draft, action.payload); }), }, }); }; // Page组件 const EditorPage = ({ instanceId }) => { const data = use(fetchData()); const dispatch = useDispatch(); const currentData = useSelector(state => state[`editor/${instanceId}`]); useEffect(() => { dispatch(createEditorSlice(instanceId).actions.setData(data)); }, [data, instanceId, dispatch]); const handleChange = (updates) => { dispatch(createEditorSlice(instanceId).actions.updateData(updates)); }; if (!currentData) return <div>加载中...</div>; return <Editor data={currentData} onChange={handleChange} />; };
每个Editor实例拥有独立的Redux存储节点,既利用了Redux的状态管理能力,又不会干扰全局状态,同时避免了数据重复存储。
额外优化建议
对于200MB+的大型JSON,建议在服务端做数据分片或按需加载,仅获取当前Editor需要的字段,避免一次性加载全部数据带来的性能压力。
内容的提问来源于stack exchange,提问作者Dellirium

