React-router下组件重载、useEffect触发时机及路由访问问题咨询
BrowserRouter与HashRouter的访问差异
你之前遇到的“BrowserRouter携带参数无法直接访问、HashRouter可以正常访问”的问题,核心是两者的路由路径存储机制不同:
- HashRouter将所有路由规则存在URL的#(哈希片段)后面,浏览器向服务器发请求时,#后面的内容完全不会被发送,服务器永远只返回根路径下的
index.html静态文件,前端再通过JS解析#后面的路由内容渲染对应页面。所以哪怕你直接输入带参数的Hash路径,也只会请求一次根路径的html文件,不会出现服务器找不到资源的404问题。 - BrowserRouter将路由路径存在域名后的正常路径段中,直接访问
https://www.mywebsite.com/page/1/2/3时,服务器会去对应路径找匹配的静态资源,找不到就直接返回404。因为单页应用的路由跳转全是前端实现的,没有对应路径的服务端资源,所以直接访问带参路由就会失败。要让BrowserRouter支持直接访问带参路由,需要服务端配置重定向规则,把所有非静态资源的请求都导向根路径的index.html。
路由参数跳转时Component是否会被完全重载?
首先明确:你配置的路由/page/:param1?/:param2?/:param3?,不管是从/#/page跳到/#/page/1/2/3,还是反向跳转,本质上都是匹配同一条Route规则。
React Router默认的组件渲染逻辑是,只要Route匹配到的组件类型引用没有变化,就不会卸载重建组件,只会给组件更新传入新的路由props(也就是新的match对象)。
你观察到组件可能重载,大概率是你现在的Route写法埋了坑:component={props => <Component {...props} />}这里用了内联箭头函数作为组件引用,这个函数在父组件每次重渲染时都是全新的对象引用,React做组件Diff时会判定这是和上一次渲染不同的组件类型,就会执行完整的卸载、重新挂载流程,和路由参数变化本身没有关系。如果把这里的写法改成直接传入稳定的组件引用component={Component},组件引用固定后,就不会再出现莫名的重建问题。
反向跳转的逻辑和正向完全一致,只要匹配同一条Route,组件引用稳定就不会重载,反之就会。
空依赖数组的useEffect(() => {}, [])为什么会多次触发?
从React的基础逻辑来说,空依赖数组的useEffect本来就是类组件componentDidMount的等价实现,只会在组件首次挂载到DOM后触发一次,组件后续因为props、state更新的重渲染不会触发它。
它多次触发的核心原因就是上面提到的内联组件引用不稳定,导致Component被反复卸载、重新挂载,每一次重新挂载都会走一次首次渲染流程,空依赖的useEffect自然会跟着触发一次,看起来就像同一个生命周期执行了好多次。
另外如果是在开发环境,React 18开启严格模式(StrictMode)时,会故意把组件挂载两次来检测副作用的清理逻辑问题,这种情况也会导致空依赖useEffect多触发一次,但这个行为只会在开发环境出现,生产环境不会复现。
依赖window.location.href的useEffect是否适合检测路由参数变化?
这个写法的触发时机是:组件重渲染时,如果读取到的window.location.href字符串和上一次渲染的值不一致,就会执行回调。但这种写法完全不适合用来检测路由参数变化,原因有三点:
- HashRouter的路由变化本质是前端JS修改了#后面的哈希部分,整个过程不会触发浏览器原生的位置变化事件,你读取的
window.location.href只是组件渲染那一刻的静态快照,根本没法和路由的更新周期保持同步。 - 只要href字符串变了就会触发回调,哪怕这个变化和路由参数毫无关系(比如你改了页面滚动锚点的hash),也会误触发。
- 加上你现在的组件本身就在因为引用不稳定被反复重建,这个effect会在每次组件重建的时候都触发一次,根本没法准确区分真实的路由参数变化。
而且React Router本身就把路由参数托管在组件的路由props里了,完全没必要绕去全局的window对象上读值。
依赖序列化props.match.params的写法是否更合适?
从触发逻辑的准确性来说,这个写法确实比上面的window.location.href写法靠谱:只有当路由的param1/param2/param3值真的发生变化时,序列化后的字符串才会不同,对应的effect才会触发,不管是正向带参加参数还是反向去参数,只要params变了就触发,不会因为无关的href变化误触发。
但这个写法还是有可以优化的空间:JSON.stringify虽然能做对象的浅对比,但存在边缘问题——如果params的字段顺序发生变化,哪怕参数值完全一样,序列化后的字符串也会不同,导致effect误触发(虽然React Router会按path定义的参数顺序返回params,日常使用一般不会遇到这个问题)。
更稳妥的写法是,先把Route上的内联组件引用改成稳定的组件引用(不要用箭头函数包裹),之后直接把需要监听的具体参数值作为依赖,比如写成useEffect(() => {}, [props.match.params.param1, props.match.params.param2, props.match.params.param3]),可读性更强,也没有序列化的边缘问题。
内容的提问来源于stack exchange,提问作者Nick

