Community JavaScript Snippet
Three React Router v4 Recipes I Inherit With Old Codebases
RRv4 still survives in long-running codebases. Three recipes I keep paged in: programmatic navigation via withRouter, query parsing, and a custom history singleton for non-React callers.
Three React Router v4 Recipes I Inherit With Old Codebases
RRv4 still survives in long-running codebases. Three recipes I keep paged in: programmatic navigation via withRouter, query parsing, and a custom history singleton for non-React callers.
By @freyadiallo
March 3, 2026
·
Updated May 18, 2026
1,001 views
31
4.1 (11)
Programmatic navigation in RRv4 is the recipe everyone needs and nobody can find in the docs on the first pass. The component must have access to props.history to call history.push, which means either the component is rendered inside a <Route render={...} /> (where router props are passed automatically) or it is wrapped in withRouter (which injects them via context). I prefer withRouter for any deeply-nested form because the consumer stays a regular function component and the navigation is trivially testable: pass a stub history in the unit test, assert history.push was called with the right arguments. The state argument to push(to, state) is the second-most-useful piece: a small object you tuck into the location stack so the destination route can read context like from: 'login' without a query string.
Three reasons I keep this in dotfiles. URLSearchParams is built into every browser and Node 18+, so the parsing has zero dependencies. The multi-value array-collapse is what makes tag-style filters work without a separate library: a URL like ?tag=a&tag=b parses to { tag: ['a', 'b'] }, and the round trip via stringifyQuery regenerates an equivalent string. The coercion step at the bottom is the load-bearing habit: every value out of URLSearchParams is a string, so a page-number param needs an explicit Number(q.page) || 1 somewhere or you will eventually see "2" + 1 === "21" show up in production. For RRv5 / RRv6 / Next this is mostly the same recipe, just attached to a different location shape.
The trick is that RRv4 wires <Router history={history}> to a single history object, and you can import that same object from anywhere in your codebase. A 401 interceptor in axios calls history.push('/login', { from: requestUrl }), the router subscribes to the listen events, and the UI re-renders the login screen, all without the interceptor ever touching React. The same pattern lets a saga finish a checkout and route to the order page in one line, which is exactly the kind of side-effect that lives outside the component tree and would otherwise need an awkward dispatch plus a route effect. The downside is that the singleton is a hidden module-level dependency: tests that exercise the interceptor have to either reset it or stub it. RRv5 keeps this exact pattern; RRv6 deprecates it in favour of useNavigate plus router-managed state.
