当 redux 状态改变时,React-Redux 在屏幕上渲染组件两次 [英] React-Redux rendering components twice on screen when redux state changes

查看:53
本文介绍了当 redux 状态改变时,React-Redux 在屏幕上渲染组件两次的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试制作具有回复功能的嵌套评论组件.当没有添加新评论时,代码工作正常,但是每当我为新评论发送操作时,组件都会在屏幕上呈现两次(给出,重复键警告).

const App = () =>{const dispatch = useDispatch();const data = useSelector((gstate) => gstate.commentsReducer.comments);const struct_data = 评论中间件(数据);//根据我的需要构建数据的实用函数.useEffect(() => dispatch(commentsAction({comments: dummy_comments })), [派遣,]);控制台日志(数据);返回 (<div className="root-app"><AddComment pid={null}/><ViewComment data={struct_data}/>

);};

减速器

const commentsReducer = (state = { comments: [] }, action) =>{开关(动作.类型){案例评论":返回 {...状态,评论:action.payload,};默认:返回状态;}};导出默认的commentsReducer;

实用功能

const commentsMiddleware = (comments) =>{常量哈希 = {};comment.forEach((c) => (hash[c.id] = c));for(让 c 评论){if (c.pid !== null) {const p = hash[c.pid];if (p.children == null) p.children = [];p.children.push(c);}}评论 = comments.filter((c) => c.pid === null);回复评论;};导出默认评论中间件;

解决方案

你的代码有几个问题:

  1. commentsMiddleware 正在改变 redux 状态
  2. 应该使用选择器对评论进行分组

如果您不想变异并且不想在添加一条评论后重新计算所有内容,您可以执行以下操作:

const { Provider, useDispatch, useSelector } = ReactRedux;const { createStore, applyMiddleware, compose } = Redux;const { createSelector } = 重新选择;const { useState, memo } = React;const id = ((num) => () => num++)(1);常量初始状态 = {注释: [],};//动作类型const ADD_COMMENT = 'ADD_COMMENT';//动作创建者const addComment = (comment) =>({类型:ADD_COMMENT,有效载荷:评论,});const reducer = (state, { type, payload }) =>{如果(类型 === ADD_COMMENT){返回 {...状态,评论:[有效负载,...state.comments],};}返回状态;};//选择器const selectComments = (state) =>状态.评论;const selectCommentsMap = createSelector([选择评论],(评论) =>评论.减少((评论,评论)=>评论集(评论 ID,评论),新地图()));const recursiveUpdate = (更新,nestedMap) =>{const recur = (更新) =>{NestedMap.set(updated.id, 更新);if (updated.id === 'root') {返回;}const parent = nestedMap.get(updated.pid);const newParent = {...父母,孩子: parent.children.map((child) =>child.id === 更新.id ?更新:孩子),};返回 recur(newParent);};返回递归(更新,nestedMap);};const addNewComment = (comment,nestedMap) =>{评论 = { ...评论,孩子:[] };NestedMap.set(comment.id, comment);const parent = nestedMap.get(comment.pid);常量更新父 = {...父母,孩子们:[评论,...parent.children],};递归更新(更新父级,nestedMap);};const selectGroupedComments = (() => {constnestedMap = new Map([['root', { id: 'root', children: [] }],]);返回创建选择器([选择评论地图],(currentMap) =>{[...currentMap.entries()].forEach(([id, comment]) => {//添加注释到nestedComments如果 (!nestedMap.get(id)) {addNewComment(评论,nestedMap);}});//我让你弄清楚如何删除评论//[...nestedMap.entries()].forEach//检查 id 是否不在 curentMap 中返回nestedMap.get('root').children;});})();//IIFE//使用 redux 开发工具创建 storeconst composeEnhancers =窗口.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ||撰写;const store = createStore(减速器,初始状态,composeEnhancers(applyMiddleware(() => (next) => (action) =>下一个(动作))));const AddComment = memo(function AddComment({pid = 'root',}) {const [text, setText] = useState('');const dispatch = useDispatch();返回 (<div><输入类型=文本"值={文本}onChange={(e) =>setText(e.target.value)}/><按钮onClick={() =>{dispatch(addComment({ text, id: id(), pid }));setText('');}}>向 {pid} 添加评论

);});const Comment = memo(function Comment({ comment }) {const { id, text, children } = 注释;console.log('渲染注释:', id);返回 (<li><h3>评论:{text},ID:{id}<Comments key={id} comments={children}/><AddComment pid={id}/>);});const 评论 = 备忘录(功能评论({ 评论}){返回 (<div>{Boolean(comments.length) &&(<ul>{comments.map((comment) => (<评论键={comment.id} 评论={comment}/>))})}

);});const App = () =>{const 评论 = useSelector(selectGroupedComments);返回 (<div><评论comments={comments}/><AddComment pid="root"/>

);};ReactDOM.render(<提供者商店={商店}><应用程序/></提供者>,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>

如果您想重新计算所有内容,代码会更简单,但如果添加一条评论,它将重新呈现所有评论,请参阅下面的隐藏片段:

const { Provider, useDispatch, useSelector } = ReactRedux;const { createStore, applyMiddleware, compose } = Redux;const { createSelector } = 重新选择;const id = ((num) => () => num++)(1);常量初始状态 = {注释: [],};//动作类型const ADD_COMMENT = 'ADD_COMMENT';//动作创建者const addComment = (comment) =>({类型:ADD_COMMENT,有效载荷:评论,});const reducer = (state, { type, payload }) =>{如果(类型 === ADD_COMMENT){返回 {...状态,评论:[有效负载,...state.comments],};}返回状态;};//选择器const selectComments = (state) =>状态.评论;const selectCommentsMap = createSelector([选择评论],(评论) =>评论.减少((评论,评论)=>评论集(评论 ID,评论),新地图()));const selectGroupedComments = createSelector([选择评论地图],(commentsMap) =>{const 复制 = 新地图([...commentsMap.entries()].concat([['root', { id: 'root', children: [] }]]).map(([key, value]) => [钥匙,//复制值并添加子项{ ...价值,孩子:[]},]));copy.forEach((value) => {如果(值.pid){//把这个孩子推到parent.children//如果当前值不是根copy.get(value.pid).children.push(value);}});返回copy.get('root').children;});//使用 redux 开发工具创建 storeconst composeEnhancers =窗口.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ||撰写;const store = createStore(减速器,初始状态,composeEnhancers(applyMiddleware(() => (next) => (action) =>下一个(动作))));const AddComment = React.memo(function AddComment({pid = 'root',}) {const [text, setText] = React.useState('');const dispatch = useDispatch();返回 (<div><输入类型=文本"值={文本}onChange={(e) =>setText(e.target.value)}/><按钮onClick={() =>{dispatch(addComment({ text, id: id(), pid }));setText('');}}>向 {pid} 添加评论

);});const Comment = React.memo(function Comment({ comment }) {const { id, text, children } = 注释;console.log('渲染注释:', id);返回 (<li><h3>评论:{text},ID:{id}<Comments key={id} comments={children}/><AddComment pid={id}/>);});const Comments = React.memo(function Comments({注释,}) {返回 (<div>{Boolean(comments.length) &&(<ul>{comments.map((comment) => (<评论键={comment.id} 评论={comment}/>))})}

);});const App = () =>{const 评论 = useSelector(selectGroupedComments);返回 (<div><评论comments={comments}/><AddComment pid="root"/>

);};ReactDOM.render(<提供者商店={商店}><应用程序/></提供者>,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>

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;

解决方案

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>

这篇关于当 redux 状态改变时,React-Redux 在屏幕上渲染组件两次的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆