契子
编码能力谁都会,重要的是,如何写出好的代码
写代码和写好的代码,不是同一个东西,这就是最佳实践的意义
最佳实践 === 好的思路, 好的思路已经成功了一半
更新: 基于此理论,落地了一个react-fabric的库,已在生产环境使用 github.com/cs-open/rea…
正文
要实现的效果,参考react-flow, 我给你 ReactFlow 组件的时候,会顺便给你一个对应的Provider组件,
优点:
- 你需要访问ReactFlow组件内部数据的时候, 会很方便, 只需要把Provider组件套在两个组件的公共外层即可,代码相当干净
- 解决非受控场景,通过ref获取数据,ref数据不全的问题,比如antd 不支持受控多列排序,因为受控非受控属性都整理到一处,所以你可以访问任意时刻的快照状态
- 细细品吧, 对代码架构有兴趣的,可以直接看react-flow 源码
import ReactFlow, { ReactFlowProvider, useNodes } from 'reactflow'
export default function Flow() {
return (
<ReactFlowProvider>
<ReactFlow nodes={...} edges={...} />
<Sidebar />
</ReactFlowProvider>
)
}
function Sidebar() {
// This hook will only work if the component it's used in is a child of a
// <ReactFlowProvider />.
const nodes = useNodes()
return (
<aside>
{nodes.map((node) => (
<div key={node.id}>
Node {node.id} -
x: {node.position.x.toFixed(2)},
y: {node.position.y.toFixed(2)}
</div>
))}
</aside>
)
}
最佳实践: stackblitz.com/edit/vitejs…
一 什么是好的组件?
- 支持组件之外访问内部状态, 不用ref
- 处理受控非受控模式, 解决掉 useUncontrolled 不支持队列的问题
- 复合组件的设计模式
- 支持多例单例模式
- 组件提供的插槽内部可以通过useStoreapi 访问和修改内部数据,而不仅仅只能读取页面上的受控状态(想想你antd是不是这样)
复杂的业务需求, 必须要有足够优秀的技术架构来支持,而技术架构,针对普通前端来说,组件就是最小单元, 如果组件写的不好,那么其他人根本无法复用你的组件,也无法扩展, 这就是很多东西屎山的由来
行文过程中, 我会列举一些项目中遇到的复杂需求,来佐证这种方案,也欢迎诸位提出更多的好问题:
假设有这么一个组件结构
<A>
<B>
<Table>
</Table>
</B>
</A>
受控模式 vs 非受控模式?
受控模式: 有回显数据的需求
非受控模式: 为了保证性能和调用方更少的维护props
非受控模式下, 一般搭配ref使用, 但是内部组件状态变更要通知外部组件怎么办?
- 维护两份数据源, 比如添加一个状态管理库, 更新时同步更新另一个数据源
- 优点: 几乎不用改动原代码
- 缺点: 需保证两份数据同步,心智负担大, 而且有些边界情况很容易出问题
- 要么改为受控模式
所以, 好的组件应该考虑到不同的使用情况, 一开始既要支持受控也要支持非受控, 如何整合呢? 查看各大组件库源码,可以发现都使用一个类似的 use-uncontrolled
const [_value, handleChange] = useUncontrolled({
value,
defaultValue,
finalValue: 'Final',
onChange,
});
但是我不推荐使用他们提供的此类hook, 它无法在复杂业务下工作, 已知的缺点:
- 不支持回调函数,无法满足同时修改多状态的场景
<Table onChange={handleChange} />
const Table = (props)=>{
const {obj} = props // obj:{country:string, city:string}
const onCountryChange = ()=>{
// bug写法
// 多次setState, 这里将导致country更新失败
props.onChange({...obj, country:''})
props.onChange({...obj, city:''})
// 正确写法 需要使用回调函数的形式
props.onChange(prev=> {...prev,country:''})
props.onChange(prev=> {...prev,city:''})
}
}
- 无法拦截转换数据
以下需求很常见, 但是写法是不支持的
const [state,setState] = useUncontrolled('')
// 比如说返回端返回的是'a,b,c'之类的格式, 但是组件需要的是['a','b','c']
const toArray = (str)=> str.split(',')
const toString = (array)=> array.join('')
return <Table value={toArray(state)} onChange={ value=> setState(toString(value))}/>
- 不止mantine, 几乎业界所有的此类hook都存在以上问题
如何解决? 越简单越好
const Table =(props)=>{
const [state,setState] = useState(props.defaultValue)
useeffect(()=>{
setState(props.value)
},[props.value])
}
但是 每个组件都要写这些东西,很不优雅,追求完美,如何优化?
既然hook不支持, 那就封装到一个 StoreUpdater 组件里, 当然这组件还有更妙的用处, 后面会提到
export const StoreUpdater = (props) => {
const { value, defaultValue, onChange} = props
const [state,setState] = useState(props.defaultValue)
useEffect(()=>{
setState(props.value)
},[props.value])
return null;
};
<StoreUpdater value defaultValue onChange/>
支持插槽访问内部数据
看一个antd的例子, dropdownRender 就是插槽,
缺点:
-
插槽内部仅能读menu数据,不能修改内部更多的数据
-
假设需要拿到
search筛选后的列表, ref也无法拿到,- 一是排序结果antd没有放在ref上
- 二是我们在哪里调用ref呢?
- 在 dropdownRender? 还是在downDoenOpen? 能保证cover住所有筛选列表变更的情况吗?
幸亏searchvalue是一个受控属性,我们可以手动执行一遍排序, 但是我们的排序逻辑敢保证和黑盒内部的一致? 得看源码了
假如你需要的数据, 不支持受控模式那直接完蛋, 比如 antd table ,只支持单列排序受控, 不支持多列排序受控, 再次刷新页面时, 不支持回显
<Select
style={{ width: 300 }}
placeholder="custom dropdown render"
dropdownRender={(menu) => (
如何解决?
Select内部 使用Provider包裹Select, 提供一个useStore的 hook给用户, 在插槽里访问内部所有状态
此方案如何支持受控模式呢?
<Wrapper value={value}>
{children}
<Row />
</Wrapper>
export const Wrapper = ({
children,
value,
defaultValue,
}: {
defaultValue?: any;
value?: any;
children: ReactNode;
}) => {
const isWrapped = useContext(StoreContext);
if (isWrapped) {
return <>{children}</>; // 做判断
}
return (
<MyProvider defaultValue={defaultValue} value={value}>
{children}
</MyProvider>
);
};
还有一个问题, 参看各组件库, 使用Provider时,就需要提供value数据,这有点问题
缺点:
- provider太顶了, 使用时, 根本没有table需要的数据
- table需要的数据太多了, 从table传参改到provider传参,这太奇怪了
优化: 组件内部调用useStore设置数据到provider
页面任意位置访问组件内部数据
假设你还没看这篇文章, 想象一下, 某一个角落的按钮需要访问table的数据, 怎么办?
- ref 这需要在顶部某个组件管理ref通信, 而且智能访问,不能监听,ref的通病
- 状态提升, 这也是官方推荐, 需要把table中该字段提到table和button的最外层,缺点:
- 改动量太大
- 敢保证数据没有依赖吗?
- 迁移之后谁来测试?
- 以后需要更多的字段,怎么办?
- 外部 store, 维护两份数据源
- 时间紧急的时候的无奈选择
你写的react组件, 或者ui组件库提供的组件, 要做到这件事, 都是不容易的
回到本文的解决办法, 只需要简单的把提供给用户的 Provider 组件放到Table的Button的最外层即可, 甚至你可以包装在多个路由外面, 跨页面做一些事情