Redux Toolkit中Moment对象序列化问题及DateRangePicker报错求助
问题复现
初始状态使用Moment对象,触发Redux序列化检查错误:
import moment from "moment"; const filtersDefaultState = { text: "", sortBy: "date", startDate: moment().startOf("month"), endDate: moment().endOf("month"), };报错:
Error: A non-serializable value was detected in the state, in the path: filters.startDate添加忽略路径配置后,Action中又出现序列化错误:
import { configureStore, createSlice } from "@reduxjs/toolkit"; import expensesReducer from "../reducers/expenses"; import filtersReducer from "../reducers/filters"; const defaultMiddlewareConfig = { serializableCheck: { ignoredPaths: ["filters.startDate", "filters.endDate"], } }; export default () => { const store = configureStore({ reducer: { expenses: expensesReducer, filters: filtersReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(defaultMiddlewareConfig), }); return store; };报错:
react_devtools_backend.js:2655 A non-serializable value was detected in an action, in the path: startDate. Value和A non-serializable value was detected in an action, in the path: endDate. Value将Moment转为ISO字符串存储,过滤逻辑触发类型错误:
import moment from "moment"; const filtersDefaultState = { text: "", sortBy: "date", startDate: moment().startOf("month").toISOString(true), endDate: moment().endOf("month").toISOString(true), };报错:
Uncaught TypeError: startDate.isSameOrBefore is not a function和Uncaught TypeError: endDate.isSameOrAfter is not a function,对应过滤逻辑:import moment from "moment"; export default (expenses, { text, sortBy, startDate, endDate }) => { return expenses .filter((expense) => { const creadtedAtMoment = moment(expense.creadtedAt); const startDateMatch = startDate ? startDate.isSameOrBefore(creadtedAtMoment, "day") : true; const endDateMatch = endDate ? endDate.isSameOrAfter(creadtedAtMoment, "day") : true; const textMatch = expense.description .toLowerCase() .includes(text.toLowerCase()); return startDateMatch && endDateMatch && textMatch; }) .sort((a, b) => { if (sortBy === "date") { return a.createdAt < b.createdAt ? 1 : -1; } else if (sortBy === "amount") { return a.amount < b.amount ? 1 : -1; } }); };DateRangePicker组件直接使用Redux状态中的值,存在格式不匹配问题:
import React from "react"; import { DateRangePicker } from "react-dates"; import { connect } from "react-redux"; import { setEndDate, setStartDate, setTextFilter, sortByAmount, sortByDate, } from "../actions/filters"; class ExpenseListfilters extends React.Component { state = { calenderFocuesd: null, }; onDatesChange = ({ startDate, endDate }) => { this.props.dispatch(setStartDate(startDate)); this.props.dispatch(setEndDate(endDate)); }; onFocusChange = (calenderFocuesd) => { this.setState(() => ({ calenderFocuesd })); }; render() { return ( <div> <input text="text" value={this.props.filters.text} onChange={(e) => { this.props.dispatch(setTextFilter(e.target.value)); }} /> <select value={this.props.filters.sortBy} onChange={(e) => { if (e.target.value === "date") { this.props.dispatch(sortByDate()); } else if (e.target.value) { this.props.dispatch(sortByAmount()); } }} > <option value="date">Date</option> <option value="amount">Amount</option> </select> <DateRangePicker startDate={this.props.filters.startDate} endDate={this.props.filters.endDate} onDatesChange={this.onDatesChange} focusedInput={this.state.calenderFocuesd} onFocusChange={this.onFocusChange} showClearDates={true} numberOfMonths={1} isOutsideRange={() => false} startDateId="MyDatePickerStart" endDateId="MyDatePickerEnd" /> </div> ); } } const mapStateToProps = (state) => { return { filters: state.filters, }; }; export default connect(mapStateToProps)(ExpenseListfilters);
解决方案
核心思路:Redux状态只存储可序列化的原始值(ISO字符串或时间戳),在需要使用Moment方法或组件适配时,做格式转换
1. 状态层存储ISO字符串
保持初始状态为ISO字符串:
import moment from "moment"; const filtersDefaultState = { text: "", sortBy: "date", startDate: moment().startOf("month").toISOString(true), endDate: moment().endOf("month").toISOString(true), };
2. 修改过滤逻辑,将字符串转为Moment对象
在调用Moment方法前,先把Redux中的ISO字符串转为Moment实例:
import moment from "moment"; export default (expenses, { text, sortBy, startDate, endDate }) => { return expenses .filter((expense) => { const creadtedAtMoment = moment(expense.creadtedAt); // 把字符串转为Moment对象 const startDateMoment = startDate ? moment(startDate) : null; const endDateMoment = endDate ? moment(endDate) : null; const startDateMatch = startDateMoment ? startDateMoment.isSameOrBefore(creadtedAtMoment, "day") : true; const endDateMatch = endDateMoment ? endDateMoment.isSameOrAfter(creadtedAtMoment, "day") : true; const textMatch = expense.description .toLowerCase() .includes(text.toLowerCase()); return startDateMatch && endDateMatch && textMatch; }) .sort((a, b) => { if (sortBy === "date") { return a.createdAt < b.createdAt ? 1 : -1; } else if (sortBy === "amount") { return a.amount < b.amount ? 1 : -1; } }); };
3. 适配DateRangePicker组件
组件中需要把Redux的ISO字符串转为Moment对象传给Picker,同时在回调中把Moment对象转为ISO字符串再dispatch:
import React from "react"; import { DateRangePicker } from "react-dates"; import { connect } from "react-redux"; import moment from "moment"; import { setEndDate, setStartDate, setTextFilter, sortByAmount, sortByDate, } from "../actions/filters"; class ExpenseListfilters extends React.Component { state = { calenderFocuesd: null, }; onDatesChange = ({ startDate, endDate }) => { // 把Moment对象转为ISO字符串再dispatch this.props.dispatch(setStartDate(startDate?.toISOString(true))); this.props.dispatch(setEndDate(endDate?.toISOString(true))); }; onFocusChange = (calenderFocuesd) => { this.setState(() => ({ calenderFocuesd })); }; render() { // 把Redux中的ISO字符串转为Moment对象 const startDate = this.props.filters.startDate ? moment(this.props.filters.startDate) : null; const endDate = this.props.filters.endDate ? moment(this.props.filters.endDate) : null; return ( <div> <input type="text" value={this.props.filters.text} onChange={(e) => { this.props.dispatch(setTextFilter(e.target.value)); }} /> <select value={this.props.filters.sortBy} onChange={(e) => { if (e.target.value === "date") { this.props.dispatch(sortByDate()); } else if (e.target.value === "amount") { this.props.dispatch(sortByAmount()); } }} > <option value="date">Date</option> <option value="amount">Amount</option> </select> <DateRangePicker startDate={startDate} endDate={endDate} onDatesChange={this.onDatesChange} focusedInput={this.state.calenderFocuesd} onFocusChange={this.onFocusChange} showClearDates={true} numberOfMonths={1} isOutsideRange={() => false} startDateId="MyDatePickerStart" endDateId="MyDatePickerEnd" /> </div> ); } } const mapStateToProps = (state) => { return { filters: state.filters, }; }; export default connect(mapStateToProps)(ExpenseListfilters);
4. 移除序列化忽略配置
现在状态和Action都是可序列化的,恢复默认Redux Toolkit配置:
import { configureStore } from "@reduxjs/toolkit"; import expensesReducer from "../reducers/expenses"; import filtersReducer from "../reducers/filters"; export default () => { const store = configureStore({ reducer: { expenses: expensesReducer, filters: filtersReducer, }, }); return store; };
补充说明
- 如果觉得ISO字符串不够轻量,也可以用时间戳(
moment().valueOf())存储,转换逻辑类似,用moment(timestamp)转回对象即可 - 确保所有操作日期的Action都只传递序列化值,避免直接传入Moment对象
内容的提问来源于stack exchange,提问作者XED

