React组件在Store数组排序后未重新渲染问题求助
问题背景
我正在学习React和Redux,跟着官方教程开发Todo应用,想添加按名称、日期对Todo列表排序的功能。目前state.sortBy能通过其他组件正常更新,用console.log(store.getState())也能看到数组已经按规则排好序,组件也订阅了store,但排序后列表组件就是不重新渲染。我试过在sortTodos返回时加.slice(0, -1)能触发渲染,但这会删掉最后一个元素,显然不是正确的解决方案,求大家帮忙。
相关代码
待办事项列表容器组件
import { connect } from 'react-redux' import TodoList from '../components/TodoList' import { toggleTodo } from '../actions' const sortTodos = (todos, sortBy) => { switch (sortBy) { case "date": return todos.sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) case "name": return todos.sort((a, b) => { if (a.name < b.name) return -1 if (a.name > b.name) return 1 return 0 }) default: return todos } } const mapStateToProps = (state) => ({ todos: sortTodos(state.todos, state.sortBy) }) const mapDispatchToProps = (dispatch) => ({ onTodoClick: (id) => dispatch(toggleTodo(id)) }) export default connect( mapStateToProps, mapDispatchToProps )(TodoList)
展示组件
import React from 'react' import Todo from './Todo' const TodoList = ({ todos, onTodoClick }) => <ul> {todos.map((todo) => <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> )} </ul> export default TodoList
问题根源
核心问题出在**Array.sort()是原地修改数组的方法**——它不会创建新数组,而是直接改变原数组的元素顺序。Redux的connect函数在对比mapStateToProps返回的props时,会做浅比较:因为你返回的todos还是原来的数组引用,即使内部元素顺序变了,浅比较会认为这个props没有变化,所以不会触发组件重新渲染。
你之前用.slice(0, -1)能生效,是因为slice()会返回一个新数组,改变了引用,让浅比较检测到变化,但这会删掉最后一个元素,显然不是正确的解决方案。
另外还要注意:你直接对state.todos调用sort()已经违反了Redux的核心原则——永远不要修改原state,这可能会导致其他依赖该state的组件出现不可预期的问题。
解决方案
解决思路很简单:在排序前先创建原数组的副本,然后对副本进行排序,这样返回的就是新数组,引用变了,connect就能检测到props变化,触发组件重新渲染。
修改后的sortTodos函数(三种可选方式)
方式1:用扩展运算符创建副本
const sortTodos = (todos, sortBy) => { // 先创建原数组的浅拷贝 const sortedTodos = [...todos]; switch (sortBy) { case "date": return sortedTodos.sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) case "name": return sortedTodos.sort((a, b) => { if (a.name < b.name) return -1 if (a.name > b.name) return 1 return 0 }) default: return sortedTodos } }
方式2:用Array.slice()创建副本
const sortTodos = (todos, sortBy) => { // slice()不传参数会返回原数组的浅拷贝 const sortedTodos = todos.slice(); switch (sortBy) { case "date": return sortedTodos.sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) case "name": return sortedTodos.sort((a, b) => { if (a.name < b.name) return -1 if (a.name > b.name) return 1 return 0 }) default: return sortedTodos } }
方式3:用Array.concat()创建副本
const sortTodos = (todos, sortBy) => { // concat()不传参数会返回原数组的浅拷贝 const sortedTodos = todos.concat(); switch (sortBy) { case "date": return sortedTodos.sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) case "name": return sortedTodos.sort((a, b) => { if (a.name < b.name) return -1 if (a.name > b.name) return 1 return 0 }) default: return sortedTodos } }
这三种方法都能创建原数组的浅拷贝,然后对拷贝后的数组进行排序并返回。这样mapStateToProps每次返回的todos都是新的引用,connect会检测到props变化,触发TodoList组件重新渲染,显示排序后的列表,同时也符合Redux不修改原state的最佳实践。
内容的提问来源于stack exchange,提问作者brozjak2

