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.

JavaScript
Frontend
3 snippets
react
design-patterns
references
freyadiallo

By @freyadiallo

March 3, 2026

·

Updated August 12, 2026

1,002 views

31

4.1 (11)

// RRv4 puts the router API on props (not on context, not on a hook). To
// navigate from a callback, you need access to props.history, which means
// the component is inside a Route render or wrapped in withRouter. The
// withRouter HOC injects { history, match, location } into ownProps.

// Stand-in router. Real RRv4 ships a BrowserHistory backed by the History API.
function createRouter(initialPath) {
    let path = initialPath || '/';
    const listeners = [];
    return {
        history: {
            push: (to, state) => {
                path = to;
                listeners.forEach((l) => l({ pathname: to, state, action: 'PUSH' }));
            },
            replace: (to, state) => {
                path = to;
                listeners.forEach((l) => l({ pathname: to, state, action: 'REPLACE' }));
            },
            goBack: () => listeners.forEach((l) => l({ pathname: path, action: 'POP' })),
            listen: (cb) => { listeners.push(cb); return () => { const i = listeners.indexOf(cb); if (i >= 0) listeners.splice(i, 1); }; },
        },
        location: { get pathname() { return path; } },
        match: { params: {}, path: '/' },
    };
}

function withRouter(Wrapped, router) {
    return function WithRouter(ownProps) {
        const merged = Object.assign({}, ownProps, {
            history: router.history,
            location: router.location,
            match: router.match,
        });
        return Wrapped(merged);
    };
}

// A consumer that needs to navigate from a button click.
function LoginForm(props) {
    return {
        type: 'form',
        props: {
            onSubmit: (event) => {
                if (event && event.preventDefault) event.preventDefault();
                // Here is the whole point: imperative navigation from inside the handler.
                props.history.push('/dashboard', { from: 'login' });
            },
        },
        children: ['login form. clicked? ' + (props.history ? 'yes' : 'no')],
    };
}

const router = createRouter('/login');
const RoutedLogin = withRouter(LoginForm, router);

const events = [];
router.history.listen((evt) => events.push(evt));

const form = RoutedLogin({});
console.log('initial render:', form.children[0]);
console.log('current pathname:', router.location.pathname);
form.props.onSubmit({ preventDefault: () => {} });
console.log('after submit, pathname:', router.location.pathname);
console.log('history events:', events);

// goBack uses the same history reference. Useful for cancel buttons.
router.history.replace('/dashboard/settings');
router.history.goBack();
console.log('all events:', events.map((e) => e.action + '->' + e.pathname));

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.