createContext
createContext要和useContext配合使用,可以理解为 "React自带的redux或mobx" ,事实上redux就是用context来实现的。但是一番操作下来我还是感觉,简单的context对视图的更新的细粒度把控比不上mobx,除非配合memo等优化手段来优化。(redux只用过一次不是很会👋)
以下demo介绍context的简单使用:
ts
// store.ts
import { createContext } from "react";
export const ThemeContext = createContext({
theme: "light",
setTheme: (_theme: string): void => {},
});
export const AuthContext = createContext({
name: "boyiao",
roleType: "admin",
setRoleType: (_roleType: string): void => {},
setName: (_name: string): void => {},
});
ts
// Demo.js
import { useContext, useState } from "react";
import { ThemeContext, AuthContext } from "./store";
const PageA = () => {
console.log("Page Render");
const { theme, setTheme } = useContext(ThemeContext);
const { name, setName, roleType, setRoleType } = useContext(AuthContext);
return (
<div>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Set Theme In Page
</button>
<div>{theme}</div>
<div>{name}</div>
</div>
);
};
const OtherPage = () => {
console.log("OtherPage Render");
return (
<div style={{ border: "1px solid red" }}>
<div>OtherPage</div>
</div>
);
};
function Demo() {
const [theme, setTheme] = useState("light");
const [name, setName] = useState("boyiao");
const [roleType, setRoleType] = useState("admin");
console.log("App Render");
return (
<>
<ThemeContext.Provider value={{ theme, setTheme }}>
<AuthContext.Provider value={{ name, setName, roleType, setRoleType }}>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Change Theme
</button>
<button onClick={() => setRoleType(roleType === "admin" ? "user" : "admin")}>
Change Auth
</button>
<Page />
</AuthContext.Provider>
</ThemeContext.Provider>
<OtherPage /> // 注意这玩意在Context之外
</>
);
}
export default Demo;
为什么说"简单的context对视图的更新的细粒度把控比不上mobx"?项目中常见的情况是,我们有一些需要在全局共享的状态需要管理如userName、roleType、themeToken等,如果用context来管理,为了保证这些变量可改变并触发视图重新渲染,我们不得不在根组件里用useState来保存这些全局变量。但是如果这些全局的变量改变,重新渲染影响到的范围是从最顶层往下的,成本未免太高。
见上面那个demo,OtherPage不会用到context中的变量,但是每次全局变量更新,OtherPage也要重新渲染。
但是当然有办法解决, 用memo、useMemo来缓存就好了;或者我们把需要共享的变量进行一些作用域的划分,即不要把所有需要共享的变量都生命在Root组件里面,这些都是为了降低重绘(排)的成本, 只是需要在别的地方下点功夫。只是如果用mobx则方便很多,因为mobx要求开发者用observer来显示地绑定要监听的组件:
ts
// store.ts
import { observable, configure, action } from "mobx";
configure({ enforceActions: "always" });
interface AccountMobx {
userName: string;
roleType: "admin" | "user";
setUserName: (name: string) => void;
setRoleType: (role: "admin" | "user" ) => void;
}
const accountMobx = observable<AccountMobx>(
{
userName: "boyiao",
roleType: "admin",
setUserName(name: string) {
this.userName = name;
},
setRoleType(role: "admin" | "user" ) {
this.roleType = role;
},
},
{
setUserName: action,
setRoleType: action
}
);
export default accountMobx;
ts
import { observer } from "mobx-react-lite";
import accountMobx from "./store";
const Demo = observer(() => {
console.log("Demo render");
return (
<div>
<div>{accountMobx.userName}</div>
<button
onClick={() => accountMobx.setUserName(accountMobx.userName + "1")}
>
set userName
</button>
</div>
);
});
export default Demo;
ts
import "./App.css";
import Demo from './ReactMobxDemo/index.tsx'
function App() {
console.log("App rendered");
return (
<>
<Demo />
<div className="App">
// App Content
</div>
</>
);
}
export default App;
这样的好处很直接:Demo的更新影响不到App中的其他内容。
错误边界
只用过类组件的,主要借助类组件的 componentDidCatch (他可以捕获子组件渲染过程中的错误,并在渲染组件树的过程中向上冒泡,直到它被捕获为止。)生命周期。函数组件要实现这种功能相对复杂。
ts
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新状态以干掉后续的渲染
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 可以在此处记录错误信息
console.error("错误捕获:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// 自定义的降级 UI
return <h1>出了点问题,请稍后再试。</h1>;
}
return this.props.children;
}
}
伟大无需多言。
createPoral(React DOM的API)
最近发布了一个图钉组件📌到npm上面,这个组件的实现就是借助了createPoral。
portal 允许组件将它们的某些子元素渲染到 DOM 中的不同位置。这使得组件的一部分可以"逃脱"它所在的容器。例如组件可以在页面其余部分上方或外部显示模态对话框和提示框。
portal 只改变 DOM 节点的所处位置。在其他方面,渲染至 portal 的 JSX 的行为表现与作为 React 组件的子节点一致。该子节点可以访问由父节点树提供的 context 对象、事件将从子节点依循 React 树冒泡到父节点。
考虑下面这个需求:在我的React应用中有一个比待标记的节点,我希望将它标记出来:即在页面的空白处打上一个「图钉」,并将「图钉」与这个「待标记的节点」用线连接(怎么画线先不管,在我的组件里我用div来做,这么简单怎么来)。
然后我希望实现的代码逻辑如下:
ts
const HomePage = () => {
return (
<div id="container">
<div id="targetDOM">这是你想要连接的元素</div>
<Thumbtack
popupContainerId="container"
targetElementId="targetDOM"
></Thumbtack>
</div>
);
};
问题来了:Thumbtack要如何封装,才能让他里面的某些DOM节点跑到#container里面,和#targetDOM产生交互?这就是createPoral的作用。
ts
const Thumbtack = ({
popupContainerId,
targetElementId
}: {
popupContainerId: string,
targetElementId: string
}) => {
const [popupContainer, setPopupContainer] = useState(null);
useEffect(() => {
const popupRef = document.getElementById(popupContainerId);
popupRef && setPopupContainer(popupRef);
}, [])
return
(<div id='thumbtack'>
{ popupContainer && createPortal(<div>📌</div>, popupContainer) }
</div>)
}
最基本的思路就如上面这样简单,把📌挂载到和#targetDOM的同一个父节点下面,方便后续的绝对定位、transform等操作。但是createPoral 改变的只是DOM元素渲染的位置,在上面那个Thumbtack中,< div>📌< /div>
可以使用在Thumbtack里面定义的变量,而且他的事件冒泡也是冒泡到#thumbtack那里,而非#container那里。
forwardRef
forwardRef最常见的还是配合useImperativeHandle一块用,但单独使用也可以把自组件的某些DOM节点暴露给父组件。我都懒得写了直接看人家React官网吧。
keep-alive在react里如何实现?
- 现有的方案:react-activation
- 推荐关注的:React18 的 Offscreen
- 自己实现:保留关键数据(如滚动的距离、需要记录的关键状态......)