下一个带有 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});
我遇到一个问题,当我将 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});