在 React 的世界里”一切都是组件“, 组件能够映射做函数式编程中的函数,React 的组件和函数同样的灵活的特性不只仅能够用于绘制 UI,还能够用于封装业务状态和逻辑,或者非展现相关的反作用, 再经过组合方式组成复杂的应用. 本文尝试解释用 React 组件的思惟来处理常见的业务开发场景.javascript
系列目录html
目录前端
响应式
编程在很长一段时期里,高阶组件都是加强和组合 React 组件的最流行的方式. 这个概念源自于函数式编程的高阶函数. 高阶组件能够定义为: 高阶组件是函数,它接收原始组件并返回原始组件的加强/填充版本:vue
const HOC = Component => EnhancedComponent;
复制代码
首先要明白咱们为何须要高阶组件:java
React 的文档说的很是清楚, 高阶组件是一种用于复用组件逻辑模式. 最为常见的例子就是 redux 的connect
和 react-router 的 withRouter
. 高阶组件最初用于取代 mixin(了解React Mixin 的前世此生). 总结来讲就是两点:react
高阶组件的一些实现方法主要有两种:git
属性代理(Props Proxy)
: 代理传递给被包装组件的 props, 对 props 进行操做. 这种方式用得最多. 使用这种方式能够作到:github
反向继承(Inheritance Inversion)
: 高阶组件继承被包装的组件. 例如:spring
function myhoc(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render();
}
};
}
复制代码
能够实现:编程
实际上高阶组件能作的不止上面列举的, 高阶组件很是灵活, 全凭你的想象力. 读者能够了解 recompose这个库, 简直把高阶组件玩出花了.
总结一下高阶组件的应用场景:
高阶组件相关文档在网上有不少, 本文不打算展开描述. 深刻了解高阶组件
高阶组件的一些规范:
包装显示名字以便于调试
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {
/* ... */
}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
复制代码
使用 React.forwardRef 来转发 ref
使用'高阶函数'来配置'高阶组件', 这样可让高阶组件的组合性最大化. Redux 的 connect 就是典型的例子
const ConnectedComment = connect(
commentSelector,
commentActions,
)(Comment);
复制代码
当使用 compose 进行组合时就能体会到它的好处:
// 🙅 不推荐
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// ✅ 使用compose方法进行组合
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同样的
const enhance = compose(
// 这些都是单独一个参数的高阶组件
withRouter,
connect(commentSelector),
);
const EnhancedComponent = enhance(WrappedComponent);
复制代码
转发全部不相关 props 属性给被包装的组件
render() {
const { extraProp, ...passThroughProps } = this.props;
// ...
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
复制代码
命名: 通常以 with*命名, 若是携带参数, 则以 create*命名
Render Props(Function as Child) 也是一种常见的 react 模式, 好比官方的 Context API 和 react-spring 动画库. 目的高阶组件差很少: 都是为了分离关注点, 对组件的逻辑进行复用; 在使用和实现上比高阶组件要简单, 在某些场景能够取代高阶组件. 官方的定义是:
是指一种在 React 组件之间使用一个值为函数的 prop 在 React 组件间共享代码的简单技术
React 并无限定任何 props 的类型, 因此 props 也能够是函数形式. 当 props 为函数时, 父组件能够经过函数参数给子组件传递一些数据进行动态渲染. 典型代码为:
<FunctionAsChild>{() => <div>Hello,World!</div>}</FunctionAsChild>
复制代码
使用示例:
<Spring from={{ opacity: 0 }} to={{ opacity: 1 }}>
{props => <div style={props}>hello</div>}
</Spring>
复制代码
某种程度上, 这种模式相比高阶组件要简单不少, 无论是实现仍是使用层次. 缺点也很明显:
再开一下脑洞. 经过一个 Fetch 组件来进行接口请求:
<Fetch method="user.getById" id={userId}>
{({ data, error, retry, loading }) => (
<Container>
{loading ? (
<Loader />
) : error ? (
<ErrorMessage error={error} retry={retry} />
) : data ? (
<Detail data={data} />
) : null}
</Container>
)}
</Fetch>
复制代码
在 React Hooks 出现以前, 为了给函数组件(或者说 dumb component)添加状态, 一般会使用这种模式. 好比 react-powerplug
官方文档
大部分状况下, 组件表示是一个 UI 对象. 其实组件不仅仅能够表示 UI, 也能够用来抽象业务对象, 有时候抽象为组件能够巧妙地解决一些问题.
举一个例子: 当一个审批人在审批一个请求时, 请求发起者是不能从新编辑的; 反之发起者在编辑时, 审批人不能进行审批. 这是一个锁定机制, 后端通常使用相似心跳机制来维护这个'锁', 这个锁能够显式释放,也能够在超过必定时间没有激活时自动释放,好比页面关闭. 因此前端一般会使用轮询机制来激活锁.
通常的实现:
class MyPage extends React.Component {
public componentDidMount() {
// 根据一些条件触发, 可能还要监听这些条件的变化,而后中止加锁轮询. 这个逻辑实现起来比较啰嗦
if (someCondition) {
this.timer = setInterval(async () => {
// 轮询
tryLock();
// 错误处理,能够加锁失败...
}, 5000);
}
}
public componentWillUnmount() {
clearInterval(this.timer);
// 页面卸载时显式释放
releaseLock();
}
public componentDidUpdate() {
// 监听条件变化,开始或中止锁定
// ...
}
}
复制代码
随着功能的迭代, MyPage 会变得愈来愈臃肿, 这时候你开始考虑将这些业务逻辑抽取出去. 通常状况下经过高阶组件或者 hook 来实现, 但都不够灵活, 好比条件锁定这个功能实现起来就比较别扭.
有时候考虑将业务抽象成为组件, 可能能够巧妙地解决咱们的问题, 例如 Locker:
/**
* 锁定器
*/
const Locker: FC<{ onError: err => boolean, id: string }> = props => {
const {id, onError} = props
useEffect(() => {
let timer
const poll = () => {
timer = setTimeout(async () => {
// ...
// 轮询,处理异常等状况
}, 5000)
}
poll()
return () => {
clearTimeout(timer)
releaseLock()
}
}, [id])
return null
};
复制代码
使用 Locker
render() {
return (<div>
{someCondition && <Locker id={this.id} onError={this.handleError}></Locker>}
</div>)
}
复制代码
这里面有一个要点:咱们将一个业务抽象为了一个组件后,业务逻辑有了和组件同样的生命周期。如今组件内部只需关心自身的逻辑,好比只关心资源请求和释放(即 How),而什么时候进行,什么条件进行(即 When)则由父级来决定, 这样就符合了单一职责原则。 上面的例子父级经过 JSX 的条件渲染就能够动态控制锁定, 比以前的实现简单了不少
我的以为 hooks 对于 React 开发来讲是一个革命性的特性, 它改变了开发的思惟和模式. 首先要问一下, "它解决了什么问题? 带来了什么新的东西?"
hooks 首先是要解决高阶组件或者 Render Props 的痛点的. 官方在'动机'上就说了:
问题: React 框架自己并无提供一种将可复用的逻辑注入到组件上的方式/原语. RenderProps 和高阶组件只是'模式层面(或者说语言层面)'的东西:
此前的方案: 高阶组件和 Render Props。这些方案都是基于组件自己的机制
hooks 如何解决:
问题:
此前的解决方法: 高阶组件和 Render Props 或者状态管理器. 分割抽离逻辑和 UI, 切割成更小粒度的组件
hooks 如何解决: Hooks 容许您根据相关部分(例如设置订阅或获取数据)将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。你还能够选择使用一个 reducer 来管理组件的本地状态,以使其更加可预测
Hooks 带来的新东西: hook 旨在让组件的内部逻辑组织成可复用的更小单元,这些单元各自维护一部分组件‘状态和逻辑’。
图片来源于twitter(@sunil Pai)
一种新的组件编写方式. 和此前基于 class 或纯函数组件的开发方式不太同样, hook 提供了更简洁的 API 和代码复用机制, 这使得组件代码变得更简短. 例如 👆 上图就是迁移到 hooks 的代码结构对比, 读者也能够看这个演讲(90% Cleaner React).
更细粒度的状态控制(useState). 之前一个组件只有一个 setState 集中式管理组件状态, 如今 hooks 像组件同样, 是一个逻辑和状态的聚合单元. 这意味着不一样的 hook 能够维护本身的状态.
无论是 hook 仍是组件,都是普通函数.
高阶组件之间只能简单嵌套复合(compose), 而多个 hooks 之间是平铺的, 能够定义更复杂的关系(依赖).
更容易进行逻辑和视图分离. hooks 自然隔离 JSX, 视图和逻辑之间的界限比较清晰, 这使得 hooks 能够更专一组件的行为.
淡化组件生命周期概念, 将原本分散在多个生命周期的相关逻辑聚合起来
一点点'响应式编程'的味道, 每一个 hooks 都包含一些状态和反作用,这些数据能够在 hooks 之间传递流动和响应, 见下文
跨平台的逻辑复用. 这是我本身开的脑洞, React hooks 出来以后尤雨溪就推了一个vue-hooks试验项目, 若是后面发展顺利, hooks 是可能被用于跨框架复用?
一个示例: 无限加载列表
通常 hooks 的基本代码结构为:
function useHook(options) {
// ⚛️states
const [someState, setSomeState] = useState(initialValue);
// ⚛️derived state
const computedState = useMemo(() => computed, [dependencies]);
// ⚛️refs
const refSomething = useRef();
// ⚛️side effect
useEffect(() => {}, []);
useEffect(() => {}, [dependencies]);
// ⚛️state operations
const handleChange = useCallback(() => {
setSomeState(newState)
}, [])
// ⚛️output
return <div>{...}</div>
}
复制代码
自定义 hook 和函数组件的代码结构基本一致, 因此有时候hooks 写着写着原来越像组件, 组件写着写着越像 hooks. 我以为能够认为组件就是一种特殊的 hook, 只不过它输出 Virtual DOM.
一些注意事项:
总结 hooks 的经常使用场景:
学习 hooks:
响应式
编程Vue
的非侵入性响应式系统是其最独特的特性之一, 能够按照 Javascript 的数据操做习惯来操做组件状态, 而后自动响应到页面中. 而 React 这边则提供了 setState, 对于复杂的组件状态, setState 会让代码变得的又臭又长. 例如:
this.setState({
pagination: {
...this.state.pagination,
current: defaultPagination.current || 1,
pageSize: defaultPagination.pageSize || 15,
total: 0,
},
});
复制代码
后来有了mobx, 基本接近了 Vue 开发体验:
@observer
class TodoView extends React.Component {
private @observable loading: boolean;
private @observable error?: Error;
private @observable list: Item[] = [];
// 衍生状态
private @computed get completed() {
return this.list.filter(i => i.completed)
}
public componentDidMount() {
this.load();
}
public render() {
/// ...
}
private async load() {
try {
this.error = undefined
this.loading = true
const list = await fetchList()
this.list = list
} catch (err) {
this.error = err
} finally {
this.loading = false
}
}
}
复制代码
其实 mobx 也有挺多缺点:
代码侵入性. 全部须要响应数据变更的组件都须要使用 observer 装饰, 属性须要使用 observable 装饰, 以及数据操做方式. 对 mobx 耦合较深, 往后切换框架或重构的成本很高
兼容性. mobx v5 后使用 Proxy 进行重构, Proxy 在 Chrome49 以后才支持. 若是要兼容旧版浏览器则只能使用 v4, v4 有一些坑, 这些坑对于不了解 mobx 的新手很难发现:
因而 hooks 出现了, 它让组件的状态管理变得更简单直接, 并且它的思想也很接近 mobx 响应式编程哲学:
状态 是驱动应用的数据. 例如 UI 状态或者业务领域状态
function Demo() {
const [list, setList] = useState<Item[]>([]);
// ...
}
复制代码
任何 源自状态而且不会再有任何进一步的相互做用的东西就是衍生。包括用户视图, 衍生状态, 其余反作用
function Demo(props: { id: string }) {
const { id } = props;
// 取代mobx的observable: 获取列表, 在挂载或id变更时请求
const [value, setValue, loading, error, retry] = usePromise(
async id => {
return getList(id);
},
[id],
);
// 衍生状态: 取代mobx的computed
const unreads = useMemo(() => value.filter(i => !i.readed), [value]);
// 衍生反作用: value变更后自动持久化
useDebounce(
() => {
saveList(id, value);
},
1000,
[value],
);
// 衍生视图
return <List data={value} onChange={setValue} error={error} loading={loading} retry={retry} />;
}
复制代码
因此说 hook 是一个革命性的东西, 它可让组件的状态数据流更加清晰. 换作 class 组件, 咱们一般的作法多是在 componentDidUpdate
生命周期方法中进行数据比较, 而后命令式地触发一些方法. 好比 id 变化时触发 getList, list 变化时进行 saveList.
hook 彷佛在淡化组件生命周期的概念, 让开发者更专一于状态的关系, 以数据流的方式来思考组件的开发. Dan Abramov在编写有弹性的组件也提到了一个原则"不要阻断数据流", 证明了笔者的想法:
不管什么时候使用 props 和 state,请考虑若是它们发生变化会发生什么。在大多数状况下,组件不该以不一样方式处理初始渲染和更新流程。这使它可以适应逻辑上的变化。
读者能够看一下awesome-react-hooks, 这些开源的 hook 方案都挺有意思. 例如rxjs-hooks, 巧妙地将 react hooks 和 rxjs 结合的起来:
function App(props: { foo: number }) {
// 响应props的变更
const value = useObservable(inputs$ => inputs$.pipe(map(([val]) => val + 1)), 200, [props.foo]);
return <h1>{value}</h1>;
}
复制代码
就如 react 官方文档说的: "咱们的 React 使用了数以千计的组件,然而却还未发现任何须要推荐你使用继承的状况。", React 偏向于函数式编程的组合模式, 面向对象的继承实际的应用场景不多.
当咱们须要将一些传统的第三方库转换成 React 组件库时, 继承就可能派上用场. 由于这些库大部分是使用面向对象的范式来组织的, 比较典型的就是地图 SDK. 以百度地图为例:
百度地图有各类组件类型: controls, overlays, tileLayers. 这些类型都有多个子类, 如上图, overlay 有 Label, Marker, Polyline 等这些子类, 且这些子类有相同的生命周期, 都是经过 addOverlay 方法来渲染到地图画布上. 咱们能够经过继承的方式将他们生命周期管理抽取到父类上, 例如:
// Overlay抽象类, 负责管理Overlay的生命周期
export default abstract class Overlay<P> extends React.PureComponent<OverlayProps & P> {
protected initialize?: () => void;
// ...
public componentDidMount() {
// 子类在constructor或initialize方法中进行实例化
if (this.initialize) {
this.initialize();
}
if (this.instance && this.context) {
// 渲染到Map画布中
this.context.nativeInstance!.addOverlay(this.instance);
// 初始化参数
this.initialProperties();
}
}
public componentDidUpdate(prevProps: P & OverlayProps) {
// 属性更新
this.updateProperties(prevProps);
}
public componentWillUnmount() {
// 组件卸载
if (this.instance && this.context) {
this.context.nativeInstance!.removeOverlay(this.instance);
}
}
// ...
// 其余通用方法
private forceReloadIfNeed(props: P, prevProps: P) {
...
}
}
复制代码
子类的工做就变得简单不少, 声明本身的属性/事件和实例化具体类:
export default class Label extends Overlay<LabelProps> {
public static defaultProps = {
enableMassClear: true,
};
public constructor(props: LabelProps) {
super(props);
const { position, content } = this.props;
// 声明支持的属性和回调
this.extendedProperties = PROPERTIES;
this.extendedEnableableProperties = ENABLEABLE_PROPERTIES;
this.extendedEvents = EVENTS;
// 实例化具体类
this.instance = new BMap.Label(content, {
position,
});
}
}
复制代码
代码来源于 react-bdmap
固然这个不是惟一的解决方法, 使用高阶组件和 hooks 一样可以实现. 只不过对于本来就采用面向对象范式组织的库, 使用继承方式会更加好理解
模态框是应用开发中使用频率很是高组件,尤为在中后台管理系统中. 可是在 React 中用着并非特别爽, 典型的代码以下:
const Demo: FC<{}> = props => {
// ...
const [visible, setVisible] = useState(false);
const [editing, setEditing] = useState();
const handleCancel = () => {
setVisible(false);
};
const prepareEdit = async (item: Item) => {
// 加载详情
const detail = await loadingDeatil(item.id);
setEditing(detail);
setVisible(true);
};
const handleOk = async () => {
try {
const values = await form.validate();
// 保存
await save(editing.id, values);
// 隐藏
setVisible(false);
} catch {}
};
return;
<>
<Table
dataSource={list}
columns={[
{
text: '操做',
render: item => {
return <a onClick={() => prepareEdit(item)}>编辑</a>;
},
},
]}
/>
<Modal visible={visible} onOk={handleOk} onCancel={handleHide}>
{/* 表单渲染 */}
</Modal>
</>;
};
复制代码
上面的代码太丑了, 不相关逻辑堆积在一个组件下 ,不符合单一职责. 因此咱们要将模态框相关代码抽取出去, 放到EditModal中:
const EditModal: FC<{ id?: string; visible: boolean; onCancel: () => void; onOk: () => void }> = props => {
// ...
const { visible, id, onHide, onOk } = props;
const detail = usePromise(async (id: string) => {
return loadDetail(id);
});
useEffect(() => {
if (id != null) {
detail.call(id);
}
}, [id]);
const handleOk = () => {
try {
const values = await form.validate();
// 保存
await save(editing.id, values);
onOk();
} catch {}
};
return (
<Modal visible={visible} onOk={onOk} onCancel={onCancel}>
{detail.value &&
{
/* 表单渲染 */
}}
</Modal>
);
};
/**
* 使用
*/
const Demo: FC<{}> = props => {
// ...
const [visible, setVisible] = useState(false);
const [editing, setEditing] = useState<string | undefined>(undefined);
const handleHide = () => {
setVisible(false);
};
const prepareEdit = async (item: Item) => {
setEditing(item.id);
setVisible(true);
};
return;
<>
<Table
dataSource={list}
columns={[
{
text: '操做',
render: item => {
return <a onClick={() => prepareEdit(item)}>编辑</a>;
},
},
]}
/>
<EditModal id={editing} visible={visible} onOk={handleHide} onCancel={handleHide}>
{' '}
</EditModal>
</>;
};
复制代码
如今编辑相关的逻辑抽取到了 EditModal 上,可是 Demo 组件还要维护模态框的打开状态和一些数据状态。一个复杂的页面可能会有不少模态框,这样的代码会变得愈来愈恶心, 各类 xxxVisible 状态满天飞. 从实际开发角度上将,模态框控制的最简单的方式应该是这样的:
const handleEdit = item => {
EditModal.show({
// 🔴 经过函数调用的方式出发弹窗. 这符合对模态框的习惯用法, 不关心模态框的可见状态. 例如window.confirm, wx.showModal().
id: item.id, // 🔴 传递数据给模态框
onOk: saved => {
// 🔴 事件回调
refreshList(saved);
},
onCancel: async () => {
return confirm('确认取消'); // 控制模态框是否隐藏
},
});
};
复制代码
这种方式在社区上也是有争议的,有些人认为这是 React 的反模式,@欲三更在Modal.confirm 违反了 React 的模式吗?就探讨了这个问题。 以图为例:
红线表示时间驱动(或者说时机驱动), 蓝线表示数据驱动。欲三更认为“哪怕一个带有明显数据驱动特点的 React 项目,也存在不少部分不是数据驱动而是事件驱动的. 数据只能驱动出状态,只有时机才能驱动出行为, 对于一个时机驱动的行为,你非得把它硬坳成一个数据驱动的状态,你不以为很奇怪吗?”. 他的观点正不正确笔者不作评判, 可是某些场景严格要求‘数据驱动’,可能会有不少模板代码,写着会很难受.
So 怎么实现?
能够参考 antd Modal.confirm的实现, 它使用ReactDOM.render
来进行外挂渲染,也有人使用Context API来实现的. 笔者认为比较接近理想的(至少 API 上看)是react-comfirm这样的:
/**
* EditModal.tsx
*/
import { confirmable } from 'react-confirm';
const EditModal = props => {
/*...*/
};
export default confirmable(EditModal);
/**
* Demo.tsx
*/
import EditModal from './EditModal';
const showEditModal = createConfirmation(EditModal);
const Demo: FC<{}> = props => {
const prepareEdit = async (item: Item) => {
showEditModal({
id: item.id, // 🔴 传递数据给模态框
onOk: saved => {
// 🔴 事件回调
refreshList(saved);
},
onCancel: async someValues => {
return confirm('确认取消'); // 控制模态框是否隐藏
},
});
};
// ...
};
复制代码
使用ReactDOM.render
外挂渲染形式的缺点就是没法访问 Context,因此仍是要妥协一下,结合 Context API 来实现示例:
扩展
Context 为组件树提供了一个传递数据的方法,从而避免了在每个层级手动的传递 props 属性.
Context 在 React 应用中使用很是频繁, 新的Context API也很是易用. Context 经常使用于如下场景:
Context 的做用域是子树, 也就是说一个 Context Provider 能够应用于多个子树, 子树的 Provider 也能够覆盖父级的 Provider 的 value. 基本结构:
import React, {useState, useContext} from 'react'
export inteface MyContextValue {
state: number
setState: (state: number) => void
}
const MyContext = React.createContext<MyContextValue>(
{
state: 1,
// 设置默认值, 抛出错误, 必须配合Provider使用
setState: () => throw new Error('请求MyContextProvider组件下级调用')
}
)
export const MyContextProvider: FC<{}> = props => {
const [state, setState] = useState(1)
return <MyContext.Provider value={{state, setState}}>{props.children}</MyContext.Provider>
}
export function useMyContext() {
return useContext(MyContext)
}
export default MyContextProvider
复制代码
Context 默认值中的方法应该抛出错误, 警告不规范的使用
扩展:
对于函数式编程范式的 React 来讲,不可变状态有重要的意义.
不可变数据具备可预测性。可不变数据可让应用更好调试,对象的变动更容易被跟踪和推导.
就好比 Redux, 它要求只能经过 dispatch+reducer 进行状态变动,配合它的 Devtool 能够很好的跟踪状态是如何被变动的. 这个特性对于大型应用来讲意义重大,由于它的状态很是复杂,若是不加以组织和约束,你不知道是哪一个地方修改了状态, 出现 bug 时很难跟踪.
因此说对于严格要求单向数据流的状态管理器(Redux)来讲,不可变数据是基本要求,它要求整个应用由一个单一的状态进行映射,不可变数据可让整个应用变得可被预测.
不可变数据还使一些复杂的功能更容易实现。避免数据改变,使咱们可以安全保留对旧数据的引用,能够方便地实现撤销重作,或者时间旅行这些功能
能够精确地进行从新渲染判断。能够简化 shouldComponentUpdate 比较。
实现不可变数据的流行方法:
笔者比较喜欢 immer,没有什么心智负担, 按照 JS 习惯的对象操做方式就能够实现不可变数据。
传统的路由主要用于区分页面, 因此一开始前端路由设计也像后端路由(也称为静态路由)同样, 使用对象配置方式, 给不一样的 url 分配不一样的页面组件, 当应用启动时, 在路由配置表中查找匹配 URL 的组件并渲染出来.
React-Router v4 算是一个真正意义上符合组件化思惟的路由库, React-Router 官方称之为‘动态路由’, 官方的解释是"指的是在应用程序渲染时发生的路由,而不是在运行应用程序以外的配置或约定中发生的路由", 具体说, <Route/>
变成了一个普通 React 组件, 它在渲染时判断是否匹配 URL, 若是匹配就渲染指定的组件, 不匹配就返回 null.
这时候 URL 意义已经不同了, URL 再也不是简单的页面标志, 而是应用的状态; 应用构成也再也不局限于扁平页面, 而是多个能够响应 URL 状态的区域(可嵌套). 由于思惟转变很大, 因此它刚出来时并不受青睐. 这种方式更加灵活, 因此选择 v4 不表明放弃旧的路由方式, 你彻底能够按照旧的方式来实现页面路由.
举个应用实例: 一个应用由三个区域组成: 侧边栏放置多个入口, 点击这些入口会加载对应类型的列表, 点击列表项须要加载详情. 三个区域存在级联关系
首先设计可以表达这种级联关系的 URL, 好比/{group}/{id}
, URL 设计通常遵循REST 风格, 那么应用的大概结构是这样子:
// App
const App = () => {
<div className="app">
<SideBar />
<Route path="/:group" component={ListPage} />
<Route path="/:group/:id" component={Detail} />
</div>;
};
// SideBar
const Sidebar = () => {
return (
<div className="sidebar">
{/* 使用NavLink 在匹配时显示激活状态 */}
<NavLink to="/message">消息</NavLink>
<NavLink to="/task">任务</NavLink>
<NavLink to="/location">定位</NavLink>
</div>
);
};
// ListPage
const ListPage = props => {
const { group } = props.match.params;
// ...
// 响应group变化, 并加载指定类型列表
useEffect(() => {
load(group);
}, [group]);
// 列表项也会使用NavLink, 用于匹配当前展现的详情, 激活显示
return <div className="list">{renderList()}</div>;
};
// DetailPage
const DetailPage = props => {
const { group, id } = props.match.params;
// ...
// 响应group和id, 并加载详情
useEffect(() => {
loadDetail(group, id);
}, [group, id]);
return <div className="detail">{renderDetail()}</div>;
};
复制代码
扩展