在上节课中我们掌握了 React 的页面级数据流,我们知道了,在单个组件内使用 useState ,不同页面建的数据是没办法共享的。 但是在实际业务中我们经常会遇到两个或者多个页面之间需要同时维护一份数据的情况,因此就有了全局的 hooks 数据流方案。
这节课我们会现将原理再讲最佳实践。
组件间数据独立
新建页面文件 src/pages/hooks.tsx
import React, { useState } from "react"; const Count = () => { const [count, setCount] = useState(1); return <div onClick={() => setCount(count + 1)}>{count}</div>; }; export default function List() { return ( <div> <Count /> <br /> <Count /> <br /> <Count /> </div> ); } 复制代码
我们编写一个简单的计数器,在页面中使用了三次,运行页面你将看到三个计数器,之间的数据是独立的,相互没有影响的。
让组件间数据同步
简单的修改一下我们的代码,将数值和方法传到组件内部,这时候我们再运行我们的页面,就会看到所有的数值都保持一致了,因为他们共用的是同一个数据。
import React, { useState } from "react"; const Count = ({ count, setCount }) => { return <div onClick={() => setCount(count + 1)}>{count}</div>; }; export default function List() { const [count, setCount] = useState(1); return ( <div> <Count count={count} setCount={setCount} /> <br /> <Count count={count} setCount={setCount} /> <br /> <Count count={count} setCount={setCount} /> </div> ); } 复制代码
但是上面的修改,我们需要将这两个属性一直往下传,如果组件层级很深的话,这么写非常的难维护。因此我们可以使用 React 的上下文来管理。
import React, { useState, createContext, useContext } from "react"; const Context = createContext<any>(null); const Count = ({}) => { const { count, setCount } = useContext(Context); return <div onClick={() => setCount(count + 1)}>{count}</div>; }; export default function List() { const [count, setCount] = useState(1); return ( <Context.Provider value={{ count, setCount }}> <Count /> <br /> <Count /> <br /> <Count /> </Context.Provider> ); } 复制代码
这样不管我们的组件嵌套多深,它都可以直接取到页面的数据,这个小技巧在开发中可以选择性的使用。
Umi 中的纯 hooks 数据流
上面的例子,我们只是将数据提升到全局维护,但在 Umi 中,它做的更多。 约定存在 src/models
目录下面的文件,只要导出了自定义 hook ,会被识别为 model,添加到全局的 hooks 数据流中。可以在任何 React 上下文中,使用 useModel
取到你需要的数据。 比如,存在文件src/models/user.tsx
,内容如下:
import { useState, useCallback } from "react"; export default function useAuthModel() { const [user, setUser] = useState("umi"); const fetchUser = useCallback(() => { setUser("umi@4 实战教学"); }, []); return { user, fetchUser, }; } 复制代码
可以直接在页面中使用
import React from "react"; import { useModel } from "umi"; export default () => { const { user, fetchUser } = useModel("user", (model) => ({ user: model.user, fetchUser: model.fetchUser, })); return <div onClick={() => fetchUser()}>hello {user}</div>; }; 复制代码
useModel
有两个参数,useModel(namespace
,updater
)。
namespace
就是 hooks model 文件的文件名,如上面例子里的 user
。
updater
- 可选参数。在 hooks model 返回多个状态,但使用组件仅引用了其中部分状态,并且希望仅在这几个状态更新时 rerender 时使用(性能相关)。
性能优化
useModel()
方法可以接受可选的第二个参数 updater
,当组件只需要使用 Model 中的部分参数,而对其它参数的变化不感兴趣时,可以传入一个函数进行过滤。以实现计数器的操作按钮为例:
// src/components/CounterActions/index.tsx import { useModel } from 'umi'; export default () => { const { add, minus } = useModel('useCounterModel', (model) => ({ add: model.increment, minus: model.decrement, })); return ( <div> <button onClick={add}>add by 1</button> <button onClick={minus}>minus by 1</button> </div> ); }; 复制代码
上面的组件并不关心计数器 Model 中的 counter
值,只需要使用 Model 提供的 increment()
和 decrement()
方法。于是我们传入了一个函数作为 useModel()
方法的第二个参数,该函数的返回值将作为 useModel()
方法的返回值。
这样,我们过滤掉了 counter
这一频繁变化的值,避免了组件重复渲染带来的性能损失。
插件实现
略,插件实现会在我们讲解完 Umi 插件开发之后,会有专门的课程讲解实现细节。
使用 Umi model 插件
pnpm i @umijs/plugins@4.0.0-rc.20 复制代码
注意指定版本号,为了这个课程在不久的将来仍然可用,我们锁定了 Umi 的版本。
增加配置 config/config.ts
,使用 model 插件,并且配置 model 来开启插件功能。
import { defineConfig } from "umi"; export default defineConfig({ plugins: [require.resolve("@umijs/plugins/dist/model")], model: {}, }); 复制代码
新建文件 src/models/user.tsx
写入上面 user.tsx
的演示代码。 新建文件 src/pages/usemodel.tsx
写入上面页面的演示代码。
运行 pnpm start
你将在页面中看到 hello umi
当你点击它时,它会变成 hello umi@4 实战教学
。