React Redux中setUsers两次触发 导致用户列表重复渲染问题
问题现象
向服务端请求用户列表接口后,页面渲染的用户数量为预期的2倍,排查发现setUsers逻辑被重复触发,无法定位根因。
相关业务代码
friends-reducer.js
const FOLLOW = 'FOLLOW'; const UNFOLLOW = 'UNFOLLOW'; const SET_USERS = 'SET_USERS'; let initialState = { users: [] }; const FriendsReducer = (state = initialState, action) => { switch (action.type) { case FOLLOW: return { ...state, users: state.users.map(u => { if (u.id === action.userId) { return { ...u, followed: true } } return u; }) } case UNFOLLOW: return { ...state, users: state.users.map(u => { if (u.id === action.userId) { return { ...u, followed: false } } return u; }) } case SET_USERS: return { ...state, users: [...state.users, ...action.users] } default: return state; } } export const followAC = (userId) => ({ type: FOLLOW, userId }) export const unfollowAC = (userId) => ({ type: UNFOLLOW, userId }) export const setUsersAC = (users) => ({ type: SET_USERS, users }) export default FriendsReducer;
FriendsConteiner.jsx
import Friends from './Friends'; import { connect } from "react-redux"; import { followAC, setUsersAC, unfollowAC } from '../../Redux/friends-Reduser'; import React from 'react'; import axios from 'axios'; class FriendsConteiner extends React.Component { componentDidMount() { if (this.props.users.length === 0) { axios.get("https://social-network.samuraijs.com/api/1.0/users").then(response => { this.props.setUsers(response.data.items) }); } } render() { return <Friends users={this.props.users} follow={this.props.follow} unfollow={this.props.unfollow} /> } } const mapStateToProps = (state) => { return { users: state.FriendsPage.users } } const mapDispatchToProps = (dispatch) => { return { follow: (userId) => { dispatch(followAC(userId)) }, unfollow: (userId) => { dispatch(unfollowAC(userId)) }, setUsers: (users) => { dispatch(setUsersAC(users)) } } } export default connect(mapStateToProps, mapDispatchToProps)(FriendsConteiner);
Friends.jsx
import React from 'react'; import User from './User'; const Friends = (props) => { return ( <div className='Friends'> <div className="selectedPage"> <span>1</span> <span>2</span> <span>3</span> <span>4</span> <span>5</span> </div> { props.users.map(u => <User followed={u.followed} id={u.id} key={u.id} photos={u.photos.smoll} fullName={u.name} follow={props.follow} unfollow={props.unfollow} />) } </div> ); } export default Friends;
User.jsx
import React from 'react'; import userPhoto from '../../assets/images/user.webp'; const User = (props) => { return ( <div className='user' key={props.id}> <div className='user__present'> <div className='user__img'><img src={props.photos != null ? props.photos : userPhoto} alt='user' /></div> <div className='user__followed'>{ props.followed ? <button onClick={() => props.unfollow(props.id)}>unfollow</button> : <button onClick={() => props.follow(props.id)}>follow</button> }</div> </div> <div className='user__info'> <div className="user__name">{props.fullName}</div> <div className="user__status">{props.status}</div> <div className='user__location'> <div className="user__city">{"props.city"}</div> <div className="user__country">{"props.country"}</div> </div> </div> </div> ); } export default User;
根因分析
两个问题叠加导致用户列表重复渲染:
- React 18 严格模式开发环境的重复执行机制:如果项目入口开启了
<React.StrictMode>,开发环境下类组件的componentDidMount会执行两次,用于检测非预期的副作用。写的if (this.props.users.length === 0)判断无法拦住重复请求——两次componentDidMount是同步执行的,此时异步接口还未返回,store里的users始终是空数组,两次判断都会通过,最终发起两次接口请求、两次触发setUsers的dispatch。 - SET_USERS处理逻辑错误:当前reducer中SET_USERS的逻辑是
users: [...state.users, ...action.users],属于追加列表而非替换列表。两次dispatch会把同一批用户数据追加两次,最终渲染数量翻倍。
修复方案
- 第一步:修正reducer中SET_USERS的逻辑,首次加载场景直接替换原有用户列表。如果后续需要支持分页加载,可以单独新增分页追加的action,或者给
setUsersAC增加参数区分操作类型:
case SET_USERS: return { ...state, users: action.users // 直接替换,不再追加 }
- 第二步:处理严格模式下的重复请求问题,二选一即可:
- 不需要严格模式的副作用检测能力:直接删除项目入口文件中包裹应用的
<React.StrictMode>标签即可。注意严格模式仅在开发环境生效,生产环境打包后不会出现重复执行问题。 - 保留严格模式:给请求增加状态锁,比如在组件实例上挂载
isRequested标记,第一次发起请求后将标记设为true,后续判断标记为true时直接终止请求逻辑;也可以用AbortController取消重复发起的请求。
- 不需要严格模式的副作用检测能力:直接删除项目入口文件中包裹应用的
额外代码问题:Friends.jsx中读取用户头像的字段
u.photos.smoll存在拼写错误,正确字段名为small,会导致头像地址读取失败。
内容的提问来源于stack exchange,提问作者Vladon305
相关产品推荐
相关产品推荐

