迁移类组件为函数组件后React Redux功能异常排查
我有一个ExportBtn组件,点击时会获取数据并导出为Excel文件。以下类组件版本可正常工作:
import React, {Component} from 'react'; import {LoaderBtn, createReport} from './utils.js'; import {connect} from 'react-redux'; import {actions} from './actions.js' class ExportBtn extends Component { btn = null; componentDidMount(){ this.btn = LoaderBtn.create(document.querySelector('#exportBtn')); } exportTable(){ this.btn.start(); this.props.fetchReportData(this.props.searchParams).then(() => { this.btn.stop(); createReport(this.props.exportedResults) }); } render(){ return ( <button id="exportBtn" onClick={this.exportTable} >{'Export'}</button> ) } } const mapStateToProps = (state) => { return { exportedResults: state.exportedResults } } const mapDispatchToProps = { fetchReportData: actions.fetchReportData } export default connect(mapStateToProps, mapDispatchToProps)(ExportBtn);
现在我将其迁移为函数组件,代码如下:
import React, { useEffect, useRef } from 'react'; import { LoaderBtn, createReport } from './utils.js'; import { connect } from 'react-redux'; import { actions } from './actions.js'; function ExportBtn(props) { const btnRef = useRef(null); useEffect(() => { btnRef.current = LoaderBtn.create(document.querySelector('#exportBtn')); }, []); function exportTable() { btnRef.current.start(); props.fetchReportData(props.searchParams).then(() => { btnRef.current.stop(); createReport(props.exportedResults); }); } return ( <button id="exportBtn" onClick={exportTable} > Export </button> ); } const mapStateToProps = (state) => { return { exportedResults: state.exportedResults }; }; const mapDispatchToProps = { fetchReportData: actions.fetchReportData }; export default connect(mapStateToProps, mapDispatchToProps)(ExportBtn);
迁移完成后,首次点击按钮时props.exportedResults为null,虽符合点击时Redux状态未填充的逻辑,但类组件版本却能正常工作。请问我是否违反Redux反模式,或是遗漏了关键步骤?我期望调用fetchReportData后props.exportedResults会被填充。
核心原因
类组件和函数组件的差异本质是闭包捕获值的特性:
- 类组件中,
this.props始终指向最新的props引用,所以在fetchReportData的then回调里,this.props.exportedResults是Redux更新后的最新值。 - 函数组件中,初始渲染时创建的
exportTable函数捕获了当时的props(此时exportedResults为null)。即使fetchReportData完成后触发组件重渲染,旧的exportTable函数依然沿用闭包里的旧props,自然拿到的是null。
另外,函数组件里用document.querySelector('#exportBtn')获取DOM元素没必要,直接用btnRef绑定DOM更可靠:
<button ref={btnRef} onClick={exportTable} > Export </button>
对应useEffect里的写法调整为:
useEffect(() => { if (btnRef.current) { btnRef.current = LoaderBtn.create(btnRef.current); } }, []);
解决闭包问题的几种方案
方案1:用useCallback绑定依赖数组
用useCallback包裹exportTable,并将相关props加入依赖数组,确保每次props更新时生成新的函数,捕获最新的props值:
const exportTable = useCallback(() => { btnRef.current.start(); props.fetchReportData(props.searchParams).then(() => { btnRef.current.stop(); createReport(props.exportedResults); }); }, [props.fetchReportData, props.searchParams, props.exportedResults]);
方案2:直接从action返回值获取数据
如果fetchReportData的action返回的Promise包含请求到的数据,直接使用该数据而非依赖props:
props.fetchReportData(props.searchParams).then((data) => { btnRef.current.stop(); createReport(data); });
这种方式跳过了组件props更新的依赖,是最直接的解决方案。
方案3:改用useSelector替代connect(推荐)
若使用React-Redux v7+,改用useSelector和useDispatch能更直观地获取最新状态,同时避免闭包问题:
import React, { useEffect, useRef, useCallback } from 'react'; import { LoaderBtn, createReport } from './utils.js'; import { useSelector, useDispatch } from 'react-redux'; import { actions } from './actions.js'; function ExportBtn(props) { const btnRef = useRef(null); const exportedResults = useSelector(state => state.exportedResults); const dispatch = useDispatch(); useEffect(() => { if (btnRef.current) { btnRef.current = LoaderBtn.create(btnRef.current); } }, []); const exportTable = useCallback(() => { btnRef.current.start(); dispatch(actions.fetchReportData(props.searchParams)).then(() => { btnRef.current.stop(); createReport(exportedResults); }); }, [dispatch, props.searchParams, exportedResults]); return ( <button ref={btnRef} onClick={exportTable} > Export </button> ); } export default ExportBtn;
总结
你没有违反Redux反模式,只是忽略了函数组件的闭包特性——类组件的this.props是动态引用,而函数组件的props会被闭包静态捕获。上述任意方案都能解决问题,其中从action返回值取数或使用useSelector的方式更简洁可靠。
内容的提问来源于stack exchange,提问作者user3905234

