下一个带有 redux 的 js 并重新选择

next js with redux and reselect

我遇到一个问题,当我将 reselect 与 next.js 和 redux 一起使用时,当 redux 状态已被 reducer 更改时,组件将不会重新呈现。

在容器中,我有一个记忆选择器函数,但选择器在容器之前被调用,并且容器在 CATEGORIES_LOADING_SUCCEEDED 操作后不会重新呈现。

当我不记住选择器时,它可以工作,但每次状态发生变化时都会重新呈现菜单。

完整代码是here

以下是一些可能相关的信息:

pages/index.js

import React from "react";
import Menu from "../components/Menu";

function HomePage() {
  return <Menu />;
}

export default HomePage;

components/Menu.js

import React, { memo, useMemo } from "react";
import { useSelector } from "react-redux";
import { selectCategoriesNested } from "../store/selectors";
import { useCategories } from "../hooks";

function Menu(props) {
  const { categories } = props;

  return (
    <pre>{JSON.stringify(categories, undefined, 2)}</pre>
  );
}
const MemoMenu = memo(Menu);
const MenuContainer = props => {
  console.log("MenuContainer start");
  useCategories();
  const categories = useSelector(selectCategoriesNested);
  const newProps = useMemo(
    () => ({
      ...props,
      categories
    }),
    [props, categories]
  );
  console.log("MenuContainer end", categories);
  return <MemoMenu {...newProps} />;
};
export default MenuContainer;

store/initStore.js(被_app.js使用)

import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunkMiddleware from "redux-thunk";
import {
  CATEGORIES_LOADING,
  CATEGORIES_LOADING_SUCCEEDED
} from "./actions";

const initState = {
  categories: {
    requested: false,
    data: {}
  }
};

const rootReducer = (state = initState, action) => {
  const { type, payload } = action;
  console.log("reducer:", type);
  if (type === CATEGORIES_LOADING) {
    return {
      ...state,
      categories: { ...state.categories, requested: true }
    };
  }
  if (type === CATEGORIES_LOADING_SUCCEEDED) {
    return {
      ...state,
      categories: {
        ...state.categories,
        data: payload.reduce((categories, category) => {
          categories[category.id] = category;
          return categories;
        }, state.categories.data)
      }
    };
  }
  return state;
};

export const initStore = (initialState = initState) => {
  return createStore(
    rootReducer,
    initialState,
    composeWithDevTools(applyMiddleware(thunkMiddleware))
  );
};

store/selectors.js

import { createSelector } from "reselect";

export const selectCategories = state => state.categories;
export const selectCategoriesRequested = createSelector(
  selectCategories,
  categories => categories.requested
);
export const selectCategoriesData = createSelector(
  selectCategories,
  categories =>
    console.log(
      "selector returning:",
      Object.keys(categories.data).length
    ) || categories.data
);

const nestCategories = data => {
  const categories = Object.values(data).map(category => ({
    ...category,
    subCategories: []
  }));
  const rootCategories = categories.filter(
    category => !category.parent
  );
  const categoriesMap = categories.reduce(
    (categories, category) =>
      categories.set(category.id, category),
    new Map()
  );
  categories.forEach(category => {
    if (
      category.parent &&
      category.parent.typeId === "category"
    ) {
      const parent = categoriesMap.get(category.parent.id);
      parent && parent.subCategories.push(category);
    }
  });
  return rootCategories;
};

// the following does not re render menu when categories
//  are loaded
export const selectCategoriesNested = createSelector(
  selectCategoriesData,
  nestCategories
);
// the following works but breaks menu as pure component
// export const selectCategoriesNested = state =>
//   nestCategories(selectCategoriesData(state));

hooks.js

import { selectCategoriesRequested } from "./store/selectors";
import { loadCategories } from "./store/actions";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";

export const useCategories = query => {
  const dispatch = useDispatch();
  const requested = useSelector(selectCategoriesRequested);
  useEffect(() => {
    if (!requested && process.browser) {
      dispatch(loadCategories(query));
    }
  }, [dispatch, query, requested]);
};

日志输出:

initStore.js:18   reducer: @@INIT
Menu.js:15        MenuContainer start
selectors.js:10   selector returning: 0
Menu.js:25        MenuContainer end []
initStore.js:18   reducer: CATEGORIES_LOADING
selectors.js:10   selector returning: 0
Menu.js:15        MenuContainer start
Menu.js:25        MenuContainer end []
initStore.js:18   reducer: CATEGORIES_LOADING_SUCCEEDED
selectors.js:10   selector returning: 1

如果我将选择器更改为:

export const selectCategoriesNested = state =>
  nestCategories(selectCategoriesData(state));

我得到 2 个额外的控制台日志:

Menu.js:15    MenuContainer start
Menu.js:25    MenuContainer end [{…}]

我想知道的是为什么选择器在before MenuContainer start 之前被调用但是选择器应该被调用from MenuContainer.

更新

添加了一些日志,看起来 nextjs 正在以某种方式改变商店。添加 logs and after CATEGORIES_LOADING_SUCCEEDED action the selector logs from Same [] to Same ["1"]. This should not be possible unless data was mutated but I can't see how my reducer 改变数据。

我可能错了,但这看起来像是一个错误,所以我提交了 bug report

这很尴尬,但我犯了错误。 reducer 确实在 reduce 函数中改变 state.categories.data:

payload.reduce((categories, category) => {
  categories[category.id] = category;
  return categories;
}, state.categories.data);

我将 state.categories.data 作为第二个参数传递给 payload.reduce 并在 reducer 函数中对其进行了修改。这个reducer函数应该是:

payload.reduce((categories, category) => {
  categories[category.id] = category;
  return categories;
}, {...state.categories.data});