React/Redux中最常用的方法,将UI状态与全局存储状态分离

uklbhaso  于 2022-11-17  发布在  React
关注(0)|答案(1)|浏览(162)

编辑:这似乎是由于在进行UI更新的同一更新中调用API的一些问题:

https://github.com/facebook/react-native/issues/32867
https://github.com/facebook/react-native/issues/35037
我有一个巨大的UI/UX问题-我有一组按钮,我想尽快更新样式,然后我还有一些其他的组件,整个加载过程可能需要相当多的时间来完成。现在,按钮需要的时间和加载组件一样长,这是可怕的UX。
这两个组件都依赖于相同的数据-我的类别切片,如下所示:

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);
  };

  // This was an unsuccessful attempt to decouple my redux store from my local 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) => {
... irrelevant now, moved all state out of here
}

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}
      //contentContainerStyle={{ justifyContent: "flex-start" }}
      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));

我最大的问题是我的UI更新-当我删除:props.categoriesSaveAll(cats)一切都是即时的,UI也是敏捷的。我已经尝试过通过useEffect钩子将数据放入本地状态来解耦UI,但这仍然不能解决问题。
由于某种原因,无论何时调用props.categoriesSaveAll(cats),无论在哪里调用它,都会导致这个问题。如果在它触发之前设置一个等待时间,UI按钮会立即更新(但在函数运行之前会冻结)。
如果我希望数据的初始填充基于全局存储,如何成功地将UI更新与全局存储保存分离?

EDIT:My 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) {
   ... some data processing here, including copying data to _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);
qacovj5a

qacovj5a1#

看起来你把自己绑在绳结上了!
我们来清理一下。
一般来说,状态应该存在于一个地方,而且只能存在于一个地方。这个地方看起来就在categoriesSlice reducer中。所以,让我们删除localCategories和selectedCategories,并派生selectedCategories。
通过删除所有不必要的useEffect挂钩,我们应该能够大幅减少额外渲染的数量。

更新:

让我们放回selectedCategories的本地状态,这样我们就可以“更新”组件ui,然后异步地向redux分派一个更新。
为了简化selectedCategories状态的初始化,让我们将组件一分为二,其中顶层组件负责加载数据,然后将其传递给select组件以初始化状态。

export const CatsModal = (props: any) => {
  /**
   * Available categories from an api
   */
  const { data, status } = useCategoriesAPIQuery({ taxon: props.taxon });

  /**
   * If the api query is pending, render a Box
   */
  if (status === 'pending') {
    return <Box />;
  }

  return <SelectCategory data={data} />;
};

const SelectCategory = ({ data }: any) => {
  /**
   * Now that react-redux hooks are standard, lets useDispatch, and useSelector, and drop the connect pattern
   */
  const dispatch = useDispatch();

  /**
   * Selected Categories in redux state
   */
  const categories = useSelector((state: RootStateOrAny) => state.categories);

  /**
   * A Category is selected if it is in the selected Categories redux state, and exists in the available Categories list
   */
  const initialState = categories.filter(
    (category) => data.find((item) => item.value === category) !== undefined
  );

  const [selectedCategories, setSelectedCategories] = useState(initialState);

  /**
   * Asyncronously update redux
   */
  const saveAll = async (categories) => dispatch(categoriesSaveAll(categories));
  
  /**
   * On Change, if the item that was pressed is selected, then unselect it, else select it.
   */
  const handlePress = (value: any) => {
    let next: any = [];
    if (selectedCategories.includes(value)) {
      next = selectedCategories.filter((item) => item !== value);
    } else {
      next = [...selectedCategories, value];
    }
    setSelectedCategories(next);
    saveAll(next);
  };
  return <MultipleSelect items={data} selectedItems={selectedCategories} onPress={handlePress} />;
};

相关问题