react-router-dom v5 同路由不同参数Link跳转页面不刷新问题
技术栈:react-router-dom v5、React 16
- 根组件渲染逻辑:
ReactDOM.render( <FirebaseContext.Provider value={new Firebase()}> <BrowserRouter> <StartApp /> </BrowserRouter>, </FirebaseContext.Provider>, document.getElementById("root") );
- 路由配置项:
{ path: "/member/:memberId", component: MemberForm, layout: "/admin" }
- Admin布局组件渲染逻辑:
return ( <> <div className="main-content" ref="mainContent"> <LoadingComponent loading={this.props.authState.loading}> <AdminNavbar {...this.props} brandText={this.getBrandText(this.props.location.pathname)} /> <AuthDetailsProvider> <Switch>{this.getRoutes(routes)}</Switch> </AuthDetailsProvider> <Container fluid> <AdminFooter /> </Container> </LoadingComponent> </div> </> )
Switch内部通过this.getRoutes方法渲染包含上述/member/:memberId在内的所有路由。
- MemberForm组件逻辑:
从其他页面首次跳转至/member/{memberid}路径时,路由可正常加载MemberForm组件,组件在componentDidMount生命周期中会根据URL中的memberId(即用户uid)发起Firebase请求拉取对应用户数据。组件内通过displayHouseholdMembers方法渲染家庭成员列表,列表项使用Link组件跳转至其他家庭成员的详情页,相关代码如下:
<Row> { this.displayHouseholdMembers() } </Row> displayHouseholdMembers = () => { const householdDetails = this.state.family; if (householdDetails) { return householdDetails.map((key, ind) => { if (key['uid'] != this.state.memberKeyID) { return ( <Row key={ind} style={{ paddingLeft: '25px', width: '50%'}}> <Col xs="5"> <Link to={ key['uid'] }> { key['first'] + " " + key['last'] } </Link> </Col> <Col xs="4"> { key['relation'] } </Col> <Col xs="3"> <Button color="primary" size="sm" onClick={(e) => this.removeHouseRelation(key)} > Remove </Button> </Col> </Row> ); } }); } };
MemberForm组件核心生命周期与渲染代码:
class MemberForm extends React.Component { constructor(props) { super(props); this.state = { ...INITIAL_STATE, currentOrganization: this.props.orgID, householdRelation: ['Spouse', 'Child', 'Parent', 'Sibling'], householdSelected: false, }; } componentDidMount() { let urlPath, personId; urlPath = "members"; personId = this.props.match.params.memberId; // 成员页默认设置状态为Active this.setState({ statusSelected: "Active" }) this.setState({ memberSaved: true, indiUid: personId }); // 生成访客入库时间 const setVisitorDate = this.readableHumanDate(new Date()); this.setState({ formType: urlPath, visitorDate: setVisitorDate }, () => { if (personId) { this.setState({ memberSaved: true, indiUid: personId }); this.getIndividualMemberInDB( this.state.currentOrganization, personId, this.state.formType, INITIAL_STATE ); } }); } // ...其余业务逻辑 return ( <> <UserHeader first={s.first} last={s.last} /> {/* 页面内容 */} <Container className="mt--7" fluid> <Row> {/* ...其余表单内容 */} <Row> { this.displayHouseholdMembers() } </Row> </Form> </CardBody> ) : null} </Card> </Col> </Row> <Row> <Col lg="12" style={{ padding: "20px" }}> <Button color="primary" onClick={e => this.submitMember(e)} size="md" > Save Profile </Button> { this.state.indiUid ? ( <Button color="secondary" onClick={e => this.disableProfile()} size="md" > Disable Profile </Button> ) : null } </Col> </Row> </Container> </> );
点击家庭成员列表中的Link组件时,浏览器地址栏URL已更新为/member/{新的UID},但页面未重新加载。
原因是跳转前后匹配的是同一路由规则/member/:memberId,React默认会复用已挂载的MemberForm组件实例,不会重新触发componentDidMount生命周期,因此不会拉取新成员数据。需要实现同路由规则下、不同路径参数跳转时,正确加载对应参数的页面内容。
方案1(推荐):在componentDidUpdate中监听路由参数变化
这是性能最优、改动最小的方案,只需要在MemberForm组件中新增componentDidUpdate生命周期,对比前后路由的memberId参数,参数变化时重置状态并重新拉取数据即可:
componentDidUpdate(prevProps) { const prevMemberId = prevProps.match.params.memberId; const currentMemberId = this.props.match.params.memberId; // 仅当memberId实际变化时执行数据拉取 if (prevMemberId !== currentMemberId) { // 先重置组件初始状态,避免上一个用户的数据残留 this.setState({ ...INITIAL_STATE }, () => { const personId = currentMemberId; const setVisitorDate = this.readableHumanDate(new Date()); this.setState({ statusSelected: "Active", memberSaved: true, indiUid: personId, currentOrganization: this.props.orgID, formType: "members", visitorDate: setVisitorDate }, () => { this.getIndividualMemberInDB( this.state.currentOrganization, personId, this.state.formType, INITIAL_STATE ); }) }) } }
注意:拉取新数据前必须重置组件状态,否则旧成员的表单值、家庭成员列表会残留到新成员页面,造成显示异常。
方案2:给路由组件绑定动态key,强制参数变化时重挂载
如果不想改动组件内部生命周期逻辑,可以修改路由渲染规则,将当前路径作为组件的key传入,路由参数变化时key会同步变化,React会自动销毁旧组件实例、挂载新组件,直接触发componentDidMount拉取数据。
修改Admin布局中的getRoutes方法,将原来直接传入component的写法改为render形式,给组件绑定key:
getRoutes = (routes) => { return routes.map((route, idx) => { if (!route.component) return null; return ( <Route path={route.path} exact={route.exact} key={idx} render={(props) => { // 用当前完整路径作为key,参数变化时key变更触发组件重挂载 return <route.component key={props.location.pathname} {...props} /> }} /> ) }) }
这种方法的缺点是每次参数变化都会完整销毁重建组件,性能稍差,适合组件状态简单、没有复杂缓存逻辑的场景。
额外注意项
当前代码中家庭成员跳转的Link组件使用了相对路径写法:<Link to={ key['uid'] }>,这种写法依赖当前页面的URL层级,一旦路由层级调整很容易出现跳转路径错误,建议改成绝对路径写法:
<Link to={`/member/${key['uid']}`}> { key['first'] + " " + key['last'] } </Link>
内容的提问来源于stack exchange,提问作者FabricioG

