上一篇把工作台的基础框架搭起来了,三栏布局能跑通,但都是静态展示。这篇开始加交互------左侧导航切换、中间列表筛选、右侧详情联动,这些看似简单的功能,做起来才发现状态管理才是真正的难点。
很多人刚接触 ArkUI 的时候,觉得 @State 就是个响应式变量,写起来和 Vue 差不多。真到了复杂页面,组件层级一多,状态传着传着就乱了,改一个地方要找半天。这篇就讲讲我们项目里实际踩坑后总结出来的状态管理思路。
问题:组件通信到底该怎么传
工作台的三栏布局涉及三个组件:NavSideBar、ContentList、DetailPanel。用户在左侧点导航,中间列表要过滤;点中间列表项,右侧详情要更新。这三个组件是平级的,谁也不拥有谁。
一开始我想用 @Prop 一层一层往下传,传了两层就发现不对------每多一层嵌套就要写一遍 @Prop,代码冗余不说,改个字段名要改好几个文件。后来改成了状态提升,把数据放在最顶层的 WorkbenchPage 里,通过回调函数往下传。
状态提升的实际写法
核心思路很简单:把共享状态放在父组件,子组件通过 @Prop 接收数据,通过回调函数通知父组件状态变化。
下面是 WorkbenchPage 的状态管理代码,文件位置在 pages/WorkbenchPage.ets:
typescript
@Entry
@Component
struct WorkbenchPage {
// 所有共享状态都放在这里
@State selectedNavId: number = 0;
@State selectedContent: ContentItem | null = null;
@State isLoading: boolean = false;
@State errorMsg: string = '';
build() {
Row() {
// 左侧导航:传入当前选中项,回调通知父组件
NavSideBar({
selectedId: this.selectedNavId,
onSelect: (id: number) => {
this.selectedNavId = id;
this.loadContentList(id);
}
})
.width(200)
// 中间列表:传入过滤后的数据和加载状态
ContentList({
items: this.filteredItems,
isLoading: this.isLoading,
errorMsg: this.errorMsg,
onItemClick: (item: ContentItem) => {
this.selectedContent = item;
}
})
.layoutWeight(1)
// 右侧详情:传入当前选中的内容
DetailPanel({
content: this.selectedContent
})
.width(360)
}
.width('100%')
.height('100%')
}
// 根据选中的导航过滤列表
private get filteredItems(): ContentItem[] {
if (this.selectedNavId === 0) {
return this.allItems;
}
return this.allItems.filter(item => item.category === this.selectedNavId);
}
private loadContentList(navId: number) {
this.isLoading = true;
this.errorMsg = '';
// 模拟网络请求
setTimeout(() => {
this.isLoading = false;
}, 500);
}
}
这里的关键点:所有会影响多个组件的状态,都放在最顶层。子组件只负责展示和发事件,不自己存业务数据。这样不管组件层级多深,改状态只需要改父组件的地方。

列表筛选与选中态
中间列表的筛选逻辑其实不复杂,就是根据选中的导航 ID 过滤数据。但有个细节:列表项的选中态要跟着右侧详情联动------点了列表项,右侧详情更新,同时列表项要高亮。
ContentList 组件里的列表项代码,文件位置在 components/ContentList.ets:
typescript
@Component
export struct ContentList {
@Prop items: ContentItem[];
@Prop isLoading: boolean;
@Prop errorMsg: string;
onItemClick: (item: ContentItem) => void = () => {};
build() {
if (this.isLoading) {
// 加载状态
Column() {
LoadingProgress()
.width(40)
.height(40)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else if (this.errorMsg) {
// 错误状态
Column() {
Text(this.errorMsg)
.fontSize(14)
.fontColor('#ff4444')
Button('重试')
.margin({ top: 12 })
.onClick(() => {
// 重试逻辑
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else {
// 正常列表
List({ space: 8 }) {
ForEach(this.items, (item: ContentItem) => {
ListItem() {
ContentCard({ item: item })
}
}, (item: ContentItem) => item.id)
}
.layoutWeight(1)
.padding(12)
}
}
}
这里把三种状态------加载中、错误、正常------都在同一个组件里处理了。父组件只需要传 isLoading 和 errorMsg,子组件自动切换显示对应的 UI。这比在父组件里写一堆 if-else 干净多了。

详情联动的细节
右侧详情面板要响应选中项的变化。一开始我直接在 DetailPanel 的 aboutToAppear 里取数据,结果发现切换列表项的时候,详情面板不更新------因为 aboutToAppear 只在组件首次创建时调用一次。
正确的做法是:DetailPanel 用 @Prop 接收 content,content 变了,UI 自动刷新。不需要手动监听,ArkUI 的响应式系统会处理。
还有个容易踩的坑:@Prop 是单向绑定,父组件改了会同步到子组件,但子组件改了不会影响父组件。如果需要双向同步,得用 @Link。这个场景里详情面板只读数据,用 @Prop 就够了。
状态管理的经验总结
做了这个页面之后,总结了几条经验:
1. 状态尽量往上提:两个以上组件共享的状态,放在它们共同的父组件里。不要在子组件里自己存一份,然后还要同步,容易出 bug。
2. 子组件要纯:子组件尽量只接收 props 和回调,不维护业务状态。这样组件复用性好,测试也方便。
3. 异常状态要单独处理:加载中、空数据、错误,这三种状态不要混在正常列表里写。单独拆出来,UI 更清晰。
4. 别滥用 @Link:@Link 虽然方便,但用多了数据流向就乱了,搞不清谁改了值。大部分场景 @Prop + 回调就够了。
小结
这篇讲了怎么用状态提升把三栏页面的交互串起来。看起来都是基础 API,但真到了复杂页面,状态管理的思路比 API 本身更重要。下一篇会讲列表性能优化,数据量大了之后怎么保证流畅度。