React Patterns
name | note |
---|---|
基础 | |
条件渲染 | ?: |
上下文 | createContext , useContext |
自定义 Hooks 复用逻辑 | useXxx |
高阶组建 | withProps(Comp) |
渲染属性 | render , children |
布局组件 | |
组合组件 | Layout.Sidebar |
展现和容器组建 | |
代理组件 | const Btn = (props)=> <button {...props}/> |
受控组建 | |
高级 | |
as 属性 自定义组建 | <Button as='a'/> |
control 属性 传递不透明上下文 | <Controller control={control}/> |
flexRender 灵活渲染组建和元素 | |
组建外状态 细粒度控制状态和渲染 | useStore() |
Hooks from Context | useTableHooks() |
Portal cross boundary |
innerRef
,elementRef
- forwardRef 透传内部引用mergeRefs
- 合并多个 ref,使得外部和内部都能拿到 refdefaultValue
,defaultValues
,initialValues
- 初始状态 props- Prop Collections and Getters
- 传递 getXyzProps 返回的 props 到 Component
- State Initializers - 状态初始器 -
<Comp createState={()=>({})}/>
- Array as children
- Style component - 同 Proxy component 但关注样式属性
- Container component
- 获取数据,传递给具体组件
- State hoisting
- 提升状态倒上层
- 例如: 表单
- State reducer pattern
- 基于 Command 操作状态
- 可以 Rewind
- 可以全局状态 - 可跟踪
- 写起来相对繁琐
Non-Rendering State Management Components
- 避免 rerender
- 状态/hook 复用
export const CurrentListTableQueryReactor = () => {
let store = useReactTableStore();
let { useListQuery } = useListQueryContext();
const { data } = useListQuery();
useEffect(() => {
if (!data) return;
store.getState().setOptions((prev) => ({
...prev,
data: data.data,
rowCount: data.total,
}));
}, [data]);
return null;
};
Portal cross boundary
- createPortal 可以跨越边界, 且共享状态
- 可以直接渲染到 window.open 的窗口
- 可以直接渲染到 iframe
- 可以直接渲染到 shadow dom
自定义 Hooks
- Custom Hook Pattern
- 逻辑复用
- 封装公共业务逻辑
控制属性
- Control Props Pattern
- 传递一个 opaque/不透明 的 controller
- 隐藏内部实现
- 例如: react-hook-form 的 control 参数
- 例如: 直接传递 zustand 的 store
- Material UI
flex render
- 允许传 Component 或者 ReactElement
- 允许覆盖或合并 props
- 例如: react-table
import React, { ReactElement, ReactNode } from 'react';
import { mergeProps as defaultMergeProps } from '@wener/reaction';
import { isReactComponent } from '@wener/reaction';
/**
* FlexRenderable maybe a component maybe an element
*
* The Component doesn't have to match the props type
*/
export type FlexRenderable<TProps> = React.ReactNode | React.ComponentType<Partial<TProps>>;
/**
* flexRender will try to render a component or a React node
*
* When passing a {@link mergeProps}, will clone the element and merge the props.
*
* @param Comp component or react node
* @param props props to pass to component
* @param mergeProps merge props to pass to component
* @see {@link https://github.com/TanStack/table/blob/af00c821b7943bc0f6d62a19b3ad514e3f315d75/packages/react-table/src/index.tsx TanStack/table}
*/
export function flexRender<TProps extends object>(
Comp: FlexRenderable<TProps>,
props: TProps,
mergeProps?: ((a: TProps, b: TProps) => TProps) | true,
): ReactNode | ReactElement {
if (!Comp) {
return null;
}
if (isReactComponent<TProps>(Comp)) {
return <Comp {...props} />;
}
// for mergeProps
{
if (mergeProps === true) {
mergeProps = flexRender.mergeProps;
}
if (typeof mergeProps === 'function' && typeof Comp === 'object' && 'props' in Comp) {
return React.cloneElement(Comp, (mergeProps as any)(Comp.props, props));
}
}
// various ReactNode types
return Comp as any;
}
flexRender.mergeProps = defaultMergeProps;
out of tree state
const CounterStore = createStore(0);
const Counter = () => {
const count = useStore(CounterStore);
return <div onClick={() => CounterStore.setState((s) => s + 1)}>{count}</div>;
};
as props
- 允许自定义组件类型
- 例如 Button 组件可以是 button, a, div, Link 等
- 例如 Container 组建可以时 form, div, fieldset 等
const Button: React.FC<{ as?: any }> = ({ as: As = 'button', ...props }) => {
return <As {...props} />;
};
高阶组件
- HOC - Higher Order Components
- 预设 props
- 对 组建进行 修饰/wrapper/decorator
const MyButton = withStyle(Button, { className: '' });
代理组件
- 实现自定义主题、样式
const Button = (props) => <button type='button' {...props} />;
渲染属性
const Node = (
<Comp
// render props
render={({ isActive }) => (isActive ? 'Active' : '')}
// props based on state
className={{ isActive } ? 'active' : ''}
>
{/* render props as children */}
{/* 函数做为子元素 - function as children */}
{({ isActive }) => (isActive ? 'Active' : 'Inactive')}
</Comp>
);
组合组件
const Layout = ({ children }) => {
return <div>{children}</div>;
};
Layout.Sidebar = ({ children }) => {
return <aside>{children}</aside>;
};
布局组件
const Layout = ({ sidebar, status, menu,children }) => (
<div>
<menu>{menu}</menu>
<aside>{sidebar}</aside>
<main>{children}</main>
<footer>{status}</aside>
</div>
);
Provider and Context
- React 的 IoC
- Provider/Context
- createContext
- 按需创建 Context
- 多个相同 Context 并存
caution
- memo 会导致 Context 变化无法被检测到
- useContext 不支持 select 可能导致性能问题 - 大状态需要注意
- 可以使用 out of tree state 解决
Controlled and Uncontrolled Components
// state = value
const MyComp = ({ state, onStateChange }) => {
return <div></div>;
};
可控组件
const MyComp: React.FC<{
value: any;
onChange?: (value: any) => void;
defaultValue?: any;
}> = (props) => {
const [value, setValue] = useControllable(props);
return <div></div>;
};
Prsentational and Container Components
- 业务逻辑和展现逻辑分离
- 业务逻辑放到 Container Components
- 展现逻辑放到 Prsentational Components
条件渲染
const Comp = ({ condition }) => {
return <>{condition ? <div>True</div> : <div>False</div>}</>;
};
相同位置组建会保留状态
const Comp = ({ condition }) => {
return (
<>
{/* 会保留状态: 因为相同组建,位置相同 */}
{condition ? <Counter name='YES' /> : <Counter name='NO' />}
{/* 不会保留状态: 位置不同 */}
{condition && <Counter name='YES' />}
{condition || <Counter name='NO' />}
</>
);
};
Hooks from Context
- 避免传递非常深的 props
- 通过 Context 传递 Hooks
- 让组件可以自行选择是否使用 Hooks
- 抽取公共组建逻辑
- 可修改 children 的逻辑
interface TableHooks<T extends {id:string}>{
// react-query
useQuery: (opts) => UseQueryResult<T>;
useSelected: () => T[];
}
const useTableHooks(){
return useContext(TableHooksContext);
}
Slot Pattern
- 组件传递逻辑层的 IoC
- 类似 WebComponents 的 slot 属性
- Pros
- 解偶 需要自上而下 传参数的问题
- 思维层更直观 - 不需要关心如何展现的,只需要关心需要展现
- 逻辑通用
- Cons
- Slot 如果不限定上下文可能会混淆 - 通过
createSlotContext
+ Compound Component 方式避免 - 直接传递 children 到别的节点会丢失 Context
- 实际 Context 不是 Slot 位置的 Context
- 可以用 Portal 解决
- 可以把 Context 放到更上层解决
- Slot 如果不限定上下文可能会混淆 - 通过
- 参考
const SlotDemo = (
<Layout>
<Layout.Slot name='Sidebar' placement='top'>
Menus
</Layout.Slot>
<Layout.Slot name='Footer' placement='right'>
Copyright
</Layout.Slot>
<h3>Layout Title</h3>
<section>Content</section>
</Layout>
);