React-Redux rendering components twice on screen when redux state changes

橙三吉。 提交于 2021-02-08 09:32:05

问题


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:

  1. commentsMiddleware is mutating redux state
  2. 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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!