🤔 反 React 范式思考
这是一个 loading 引发的故事
如何设计一个符合范式的 Loading
const App = () => {
return <>
{/** DO SOME THING **/}
<Loading />
</>
}
const Loading = () => {
const { loadingVisible } = useStore()
return loadingVisible ? '转圈圈' : null
}
我们在 store 中去定义 loading 的状态, 然后通过 action, transform, mutation, change, 之类的操作去改变 store, 使 react 驱动 loading 显示
这么做很符合数据驱动视图, 但是对应存在下面这些问题
-
不符合直觉, 直觉是我们打开和关闭 loading 而不是改变 store 中的某个状态
-
loading 在最初加载不管 store 还是 view 都会存在前置加载的其余资源
-
store 层, 如果放在顶层 store , store的状态机会变复杂, 如果分散在子 store 同样面临 store 管理的问题, 虽然这个问题状态管理的通病
反范式的 Loading
// file - loading.jsx
const Loading = () => <>转圈圈</>
export let loadingVisible = false
const loadingElm = document.createElement("div")
export function showLoading(){
document.body.appendChild(loadingElm);
ReactDOM.render(<Loading />, loadingElm);
loadingVisible = true
}
export function hideLoading(){
ReactDOM.unmountComponentAtNode(loadingElm);
document.body.removeChild(loadingElm);
loadingVisible = false
}
这种方法相当与我们直接操作 dom 去生成, 当然同样可以用这种方法来实现 toast alert 一些全局的通用组件
好处是变成了更符合直觉的命令式编程
命令式的 React
将这种写法进一步延伸, 我们可以做些什么
Demo 地址 https://codesandbox.io/s/beautiful-bhabha-8eb0h?file=/index.js
function confirmModal({ title = "", context }) {
return new Promise((resolve, reject) => {
const div = document.createElement("div");
document.body.appendChild(div);
const close = () =>
ReactDOM.unmountComponentAtNode(div) && document.body.removeChild(div);
const onOk = () => close() && resolve();
const onCancel = () => close() && reject();
ReactDOM.render(
<Modal title={title} visible={true} onOk={onOk} onCancel={onCancel}>
{context}
</Modal>,
div
);
});
}
async function confirm() {
// 检测一些
try {
await confirmModal({ title: "是否确认", context: "是否确认信息" });
console.log("确认");
} catch (error) {
console.log("取消");
}
}
ReactDOM.render(
<div className="App">
<Button onClick={confirm}>确认</Button>
</div>,
document.getElementById("root")
);
通过 Promise 我们可以把一些原本需要写在局部组建内的一些状态控制变成, 更加符合直觉的命令式逻辑, 并把业务逻辑的主线是变得更加清晰
async function createItem() {
// 检测一些
const div = document.createElement("div");
document.body.appendChild(div);
const close = () =>
ReactDOM.unmountComponentAtNode(div) && document.body.removeChild(div);
const onFinish = async () => {
// await 更新接口
close();
};
const onFinishFailed = () => {
// 错误提示
};
ReactDOM.render(
<Modal title={"新建"} visible={true} footer={null}>
<Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: "Please input your username!" }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: "Please input your password!" }]}
>
<Input.Password />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Modal>,
div
);
}
通过主动控制 React 的创建和销毁实现脱离当前文档流的逻辑
跨树的状态管理
因为我们通过 ReactDOM.render 创建了新的 react 节点树, 所以原有基于 ReactContext 的跨节点通信会失效, 所以需要跨 react 树通信的方式
可以直接通过类似 react-redux 等外部状态管理
demo: https://codesandbox.io/s/vibrant-shamir-qg97s?file=/index.js
import { Provider } from "react-redux";
<Provider store={reduxStore}>
{/* 主线业务 */}
<App />
</Provider>
<Provider store={reduxStore}>
{/* 我们当前要做事情的组件 */}
<DoSomeThing />
</Provider>
当然直接嵌套用主线业务的 store 可以完成通信, 但是很多时候未免显得太重, 毕竟包裹了一层
然后我找了一些 React 状态管理的库做参考 参考列表
设计了下面这个跨树跨组件通信的方法, 通过创建一个隐藏的 React tree 来包裹 一个或一段 hooks 的集合, 并共享这段 hooks 到消费的组件
demo https://codesandbox.io/s/goofy-kilby-gyi2o?file=/src/index.js
/**
* 创建跨组件跨树通讯 Hooks (可以用于的跨组件使用)
* 思路
* 1. 通过创建一个隐藏 React Tree 来包裹 Hook
* 2. Hook 变化触发隐藏 React Tree 渲染
* 3. 返回的是一个被劫持的 Hook 当 隐藏 React Tree 渲染 时会更新劫持的 Hook 数据
* @param {Function} hook 自定义的 useHooks
*/
export const createHookObserver = (hook) => {
const div = document.createElement("div");
const events = new Set();
let $data;
const update = (data) => {
$data = data;
events.forEach((event) => event(data));
return null;
};
render(
createElement(() => update(hook())),
div
);
const useHooks = () => {
const [val, setVal] = useState($data);
useEffect(() => {
events.add(setVal);
return () => events.delete(setVal);
}, []);
return val;
};
return useHooks;
};
const useCount = createHookObserver(() => {
const [count, setCount] = useState(0);
return { count, setCount };
});
聊聊 React 设计
在 React 中所谓组件, 其实就是状态机器, 通过定义 state 推导出在当前状态下的视图状态.
设计 React 组件其实是在定义状态, 描述逻辑和现实的抽象.
我们能从 React 组件设计中体会到那种 less is more 的简洁美, 但绕回来说, 软件工程没有银弹.
当我们遇到的逻辑和现实问题不能够或很麻烦通过 React 那种简洁富有张力的描述方式去解决时, 我们应该怎么取舍
原教旨主义, 范式, 在什么样的场景下应该打破这些束缚
End
跳出框架来思考一些问题吧, 我们使用这个工具, 概念来解决我们的具体问题, 不应该这些东西限制住.