问题
I am trying to make a nested comments component, with reply functionality. The code works fine when no new comment is added, but whenever I dispatch the action for new-comment, the components get rendered twice on the screen(Giving, duplicate key warning).
const App = () => {
const dispatch = useDispatch();
const data = useSelector((gstate) => gstate.commentsReducer.comments);
const struct_data = commentsMiddleware(data); // A utility function to structure the data as per my need.
useEffect(() => dispatch(commentsAction({ comments: dummy_comments })), [
dispatch,
]);
console.log(data);
return (
<div className="root-app">
<AddComment pid={null} />
<ViewComment data={struct_data} />
</div>
);
};
Reducer
const commentsReducer = (state = { comments: [] }, action) => {
switch (action.type) {
case "comments":
return {
...state,
comments: action.payload,
};
default:
return state;
}
};
export default commentsReducer;
Utility Function
const commentsMiddleware = (comments) => {
const hash = {};
comments.forEach((c) => (hash[c.id] = c));
for (let c of comments) {
if (c.pid !== null) {
const p = hash[c.pid];
if (p.children == null) p.children = [];
p.children.push(c);
}
}
comments = comments.filter((c) => c.pid === null);
return comments;
};
export default commentsMiddleware;
回答1:
Your code has a couple of problems:
- commentsMiddleware is mutating redux state
- Grouping comments should be done with a selector
If you don't want to mutate and you don't want to recalculate everything if one comment is added you can do the following:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const { useState, memo } = React;
const id = ((num) => () => num++)(1);
const initialState = {
comments: [],
};
//action types
const ADD_COMMENT = 'ADD_COMMENT';
//action creators
const addComment = (comment) => ({
type: ADD_COMMENT,
payload: comment,
});
const reducer = (state, { type, payload }) => {
if (type === ADD_COMMENT) {
return {
...state,
comments: [payload, ...state.comments],
};
}
return state;
};
//selectors
const selectComments = (state) => state.comments;
const selectCommentsMap = createSelector(
[selectComments],
(comments) =>
comments.reduce(
(comments, comment) =>
comments.set(comment.id, comment),
new Map()
)
);
const recursiveUpdate = (updated, nestedMap) => {
const recur = (updated) => {
nestedMap.set(updated.id, updated);
if (updated.id === 'root') {
return;
}
const parent = nestedMap.get(updated.pid);
const newParent = {
...parent,
children: parent.children.map((child) =>
child.id === updated.id ? updated : child
),
};
return recur(newParent);
};
return recur(updated, nestedMap);
};
const addNewComment = (comment, nestedMap) => {
comment = { ...comment, children: [] };
nestedMap.set(comment.id, comment);
const parent = nestedMap.get(comment.pid);
const updatedParent = {
...parent,
children: [comment, ...parent.children],
};
recursiveUpdate(updatedParent, nestedMap);
};
const selectGroupedComments = (() => {
const nestedMap = new Map([
['root', { id: 'root', children: [] }],
]);
return createSelector(
[selectCommentsMap],
(currentMap) => {
[...currentMap.entries()].forEach(([id, comment]) => {
//add comment to nestedComments
if (!nestedMap.get(id)) {
addNewComment(comment, nestedMap);
}
});
//I let you figure out how to remove a comment
// [...nestedMap.entries()].forEach
// check if id is not in curentMap
return nestedMap.get('root').children;
}
);
})(); //IIFE
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const AddComment = memo(function AddComment({
pid = 'root',
}) {
const [text, setText] = useState('');
const dispatch = useDispatch();
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button
onClick={() => {
dispatch(addComment({ text, id: id(), pid }));
setText('');
}}
>
Add comment to {pid}
</button>
</div>
);
});
const Comment = memo(function Comment({ comment }) {
const { id, text, children } = comment;
console.log('rendering comment:', id);
return (
<li>
<h3>
comment: {text}, id: {id}
</h3>
<Comments key={id} comments={children} />
<AddComment pid={id} />
</li>
);
});
const Comments = memo(function Comments({ comments }) {
return (
<div>
{Boolean(comments.length) && (
<ul>
{comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
</ul>
)}
</div>
);
});
const App = () => {
const comments = useSelector(selectGroupedComments);
return (
<div>
<Comments comments={comments} />
<AddComment pid="root" />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
If you want to recalculate everything the code will be simpler but it will re render all comments if one comment is added, see hidden snippet below:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const id = ((num) => () => num++)(1);
const initialState = {
comments: [],
};
//action types
const ADD_COMMENT = 'ADD_COMMENT';
//action creators
const addComment = (comment) => ({
type: ADD_COMMENT,
payload: comment,
});
const reducer = (state, { type, payload }) => {
if (type === ADD_COMMENT) {
return {
...state,
comments: [payload, ...state.comments],
};
}
return state;
};
//selectors
const selectComments = (state) => state.comments;
const selectCommentsMap = createSelector(
[selectComments],
(comments) =>
comments.reduce(
(comments, comment) =>
comments.set(comment.id, comment),
new Map()
)
);
const selectGroupedComments = createSelector(
[selectCommentsMap],
(commentsMap) => {
const copied = new Map(
[...commentsMap.entries()]
.concat([['root', { id: 'root', children: [] }]])
.map(([key, value]) => [
key,
//copy the value and add children
{ ...value, children: [] },
])
);
copied.forEach((value) => {
if (value.pid) {
//push this child to parent.children
// if current value is not root
copied.get(value.pid).children.push(value);
}
});
return copied.get('root').children;
}
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const AddComment = React.memo(function AddComment({
pid = 'root',
}) {
const [text, setText] = React.useState('');
const dispatch = useDispatch();
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button
onClick={() => {
dispatch(addComment({ text, id: id(), pid }));
setText('');
}}
>
Add comment to {pid}
</button>
</div>
);
});
const Comment = React.memo(function Comment({ comment }) {
const { id, text, children } = comment;
console.log('rendering comment:', id);
return (
<li>
<h3>
comment: {text}, id: {id}
</h3>
<Comments key={id} comments={children} />
<AddComment pid={id} />
</li>
);
});
const Comments = React.memo(function Comments({
comments,
}) {
return (
<div>
{Boolean(comments.length) && (
<ul>
{comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
</ul>
)}
</div>
);
});
const App = () => {
const comments = useSelector(selectGroupedComments);
return (
<div>
<Comments comments={comments} />
<AddComment pid="root" />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
来源:https://stackoverflow.com/questions/64928800/react-redux-rendering-components-twice-on-screen-when-redux-state-changes