React/Redux解耦UI状态与全局存储状态,解决UI卡顿问题
编辑补充:该问题疑似和UI更新时同步调用API有关。
我现在遇到严重的UI/UX问题:有一组按钮需要快速更新样式,但另外有个组件加载过程耗时很长,现在按钮的更新速度和这个加载慢的组件同步,用户体验特别差。
两个组件依赖同一个Redux的categories切片数据,切片代码如下:
import { createSlice } from "@reduxjs/toolkit"; const initialState = []; export const categoriesSlice = createSlice({ name: "categories", initialState, reducers: { categoriesSave: (state, action) => { state = [...state, action.payload]; return state; }, categoriesSaveAll: (state, action) => { if (state === action.payload) { return state; } return action.payload; }, }, }); export const { categoriesSave, categoriesSaveAll } = categoriesSlice.actions; export default categoriesSlice.reducer;
我的分类选择组件代码如下:
const CatsModalChild = (props: any) => { const reduxCategories = useSelector( (state: RootStateOrAny) => state.categories ); const [localCategories, setLocalCategories] = useState<any[]>([]); const categories = useCategoriesAPIQuery({ taxon: props.taxon }); const [selectedCategories, setSelectedCategories] = useState<string[]>([]); const categoryChange = (value: any) => { let _selectedCategories = [...selectedCategories]; if (_selectedCategories.indexOf(value) === -1) { _selectedCategories.push(value); } else { _selectedCategories = _selectedCategories.filter( (item) => item !== value ); } setSelectedCategories(_selectedCategories); }; const onCategoriesSelectionsChange = () => { if (categories.status === "fulfilled") { let selectedCats = selectedCategories.map((x: any) => x.value); let deleteCats = new Set(categories.data.map((x: any) => x.value)); // @ts-ignore const difference = localCategories.filter((x) => !deleteCats.has(x)); const cats = [...selectedCats, ...difference]; props.categoriesSaveAll(cats); } }; const categoriesMapper = () => { let _selectedCategories: any = []; for (let cat of localCategories) { let catVal = categories.data.filter((item: any) => item.value === cat); let selectedCat = catVal[0]; if (selectedCat) { _selectedCategories.push(selectedCat); } } setSelectedCategories(_selectedCategories); }; // 尝试解耦Redux和本地state但未成功 useEffect(() => { const _localCategories = [...reduxCategories]; setLocalCategories(_localCategories); }, []); useEffect(() => { onCategoriesSelectionsChange(); }, [selectedCategories, categories]); useEffect(() => { if (categories.data?.length > 0) { categoriesMapper(); } }, [categories]); if (categories.status == "pending") { return <Box />; } return ( <MultipleSelect items={categories.data} selectedItems={selectedCategories} onPress={categoryChange} /> ); }; const mapStateToProps = (state: any) => { ... 已移除此处无关逻辑 } function mapDispatchToProps(dispatch: any) { let actions = bindActionCreators( { categoriesSaveAll, }, dispatch ); return { ...actions, dispatch }; } export const CatsModal = connect( mapStateToProps, mapDispatchToProps )(memo(CatsModalChild));
文章列表组件代码如下:
const Articles = (props) => { return ( <ScrollView minH={600} zIndex={10} _dark={{ bg: "gray.700", }} _light={{ bg: "gray.100", }} > <Helmet> <meta charSet="utf-8" /> <title>{props.title}</title> <link rel="canonical" href="http://pandaist.com/news" /> </Helmet> <Center py={[25, 25, 25, 50, 50]}> <Heading fontSize={["4xl", "6xl", "6xl"]} _light={{ color: "black", }} _dark={{ color: "white", }} > {props.title} </Heading> </Center> <Filters hideCalendar={props.hideCalendar} /> <RecentArticles {...props} title="Recent Articles" taxon={props.taxon} /> <Box alignItems="center" justifyContent="center"> <Categories taxon={props.taxon} /> </Box> <CategoryArticles {...props} taxon={props.taxon} /> <Box h={50} /> </ScrollView> ); }; const mapStateToProps = (state) => { return { settings: state.settings, category: state.categories, resetdate: state.resetdate, date: state.date, readarticles: state.readarticles, }; }; function mapDispatchToProps(dispatch) { let actions = bindActionCreators({ settingsSave }, dispatch); return { ...actions, dispatch }; } export default connect(mapStateToProps, mapDispatchToProps)(memo(Articles));
核心问题:
移除props.categoriesSaveAll(cats)后,按钮UI响应立刻变快;但调用这个方法时,按钮更新就会延迟。我试过用useEffect把Redux数据存到本地state来解耦,但没成功;设置延迟执行虽然能让按钮即时更新,但会卡住直到函数执行完成。
现在需要在基于全局存储初始化数据的前提下,成功把UI更新和全局存储的保存操作解耦。
编辑补充:以下是实际发起API查询的CategoryArticles组件代码:
const CategoryArticlesChild = (props) => { const [isLoading, setIsLoading] = useState(true); const [articleGroups, setArticleGroups] = useState([]); const [query, setQuery] = useState(false); const { data } = useCategoryArticlesAPIQuery(query ?? skipToken); const loadArticlesByCategory = async () => { const hsks = await getLevels(); let _query = { category: props.settings.cats, limit: 15, ordering: "-date", published: 1, display_skill: hsks, taxon: props.taxon, }; _query = processDates(props, _query); setQuery(_query); }; const processMetaData = async (data) => { if (data) { ... 此处为数据处理逻辑,将数据复制到_data setArticleGroups(_data); } }; useEffect(() => { processMetaData(data); }, [data]); useEffect(() => { loadArticlesByCategory(); }, []); if (isLoading || articleGroups?.length === 0) { return ( <Flex flexGrow={1} _dark={{ bg: "gray.700", }} _light={{ bg: "gray.100", }} alignSelf="center" justifyContent="center" w="100%" minH={500} > <Loading /> </Flex> ); } return ( <FlatList zIndex={-1} data={articleGroups} renderItem={(articleGroup, index) => ( <CardRow articleGroup={articleGroup.item} count={index} key={index} cardForm="WhiteCard" /> )} /> ); }; const CategoryArticles = memo(CategoryArticlesChild);
解决办法
异步更新Redux,不阻塞UI
把Redux的更新操作放到异步队列里,让UI先更新,后台再处理全局存储的保存:const onCategoriesSelectionsChange = () => { if (categories.status === "fulfilled") { let selectedCats = selectedCategories.map((x: any) => x.value); let deleteCats = new Set(categories.data.map((x: any) => x.value)); const difference = localCategories.filter((x) => !deleteCats.has(x)); const cats = [...selectedCats, ...difference]; // 用queueMicrotask异步执行,不卡UI queueMicrotask(() => { props.categoriesSaveAll(cats); }); } };修正本地state的同步逻辑
原来的useEffect只在组件挂载时初始化localCategories,Redux数据更新时不会同步,把reduxCategories加入依赖数组:useEffect(() => { setLocalCategories([...reduxCategories]); }, [reduxCategories]);这样本地state能和Redux保持同步,同时按钮UI只依赖本地的selectedCategories,不会被Redux更新拖慢。
优化CategoryArticles的API调用逻辑
当categories更新时,CategoryArticles会触发重渲染和新的API请求,这是卡顿的关键原因之一。用useMemo缓存查询参数,避免不必要的请求:const loadArticlesByCategory = async () => { const hsks = await getLevels(); // 缓存查询参数,只有依赖变化时才重新生成 const _query = useMemo(() => ({ category: props.settings.cats, limit: 15, ordering: "-date", published: 1, display_skill: hsks, taxon: props.taxon, }), [props.settings.cats, props.taxon, hsks]); const processedQuery = processDates(props, _query); setQuery(processedQuery); };另外在
processMetaData里添加setIsLoading(false),避免一直显示加载状态。减少不必要的组件重渲染
给useSelector加上shallowEqual,只有当categories数组内容真的变化时才更新组件:import { shallowEqual, useSelector } from 'react-redux'; const reduxCategories = useSelector( (state: RootStateOrAny) => state.categories, shallowEqual );
内容的提问来源于stack exchange,提问作者Steven Matthews

