redux-first-history

repository·master·Indexed 19 days ago

https://github.com/salvoravida/redux-first-history

A Redux history binding that makes the Redux store the single source of truth for routing. It supports multiple routing libraries including react-router (v5 and v6), @reach/router, and wouter, eliminating synchronization issues by tunneling all routing information to state.router.location. Version 5.2.0 provides middleware, reducers, and action creators like push(), replace(), and goBack() to manage navigation via the Redux store.

Tokens
7.1K
Snippets
21
Records
26
Agent score
66%

What's inside redux-first-history

  1. How redux-first-history works: The single source of truth

    master

    The core goal of redux-first-history is to make the Redux store the 100% single and only source of truth for routing.

    In traditional setups, components might get location from Redux, others from Router context, and others from HOCs, leading to synchronization issues. With redux-first-history, all routing information is tunneled to state.router.location. This ensures that regardless of which router library you use (react-router, @reach/router, wouter, etc.), the location state is always consistent across the entire application. This eliminates synchronization issues and improves React shallowCompare rendering optimizations.

  2. Configure redux-first-history with @reduxjs/toolkit

    master

    If you are using Redux Toolkit, integrate the middleware into the configureStore setup.

    import { combineReducers } from "redux";
    import { configureStore } from "@reduxjs/toolkit";
    import { createReduxHistoryContext } from "redux-first-history";
    import { createBrowserHistory } from "history";
    
    const {
      createReduxHistory,
      routerMiddleware,
      routerReducer
    } = createReduxHistoryContext({ history: createBrowserHistory() });
    
    export const store = configureStore({
      reducer: combineReducers({
        router: routerReducer
      }),
      middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(routerMiddleware),
    });
    
    export const history = createReduxHistory(store);
  3. Integrate with React Router (v5 and v6)

    master

    To use the history in your React components, wrap your application in a Router component provided by redux-first-history.

    // React Router v5
    import { Provider } from "react-redux";
    import { Router } from "react-router-dom";
    import { store, history } from "./store";
    
    const App = () => (
      <Provider store={store}>
        <Router history={history}>
          {/* ... */}
        </Router>
      </Provider>
    );
    
    // React Router v6
    import { Provider } from "react-redux";
    import { HistoryRouter as Router } from "redux-first-history/rr6";
    import { store, history } from "./store";
    
    const App = () => (
      <Provider store={store}>
        <Router history={history}>
          {/* ... */}
        </Router>
      </Provider>
    );
  4. Configure redux-first-history with Redux

    master

    To set up the library, use createReduxHistoryContext to generate the necessary middleware and reducer. You must provide a history object (typically from the history package).

    import { createStore, combineReducers, applyMiddleware } from "redux";
    import { composeWithDevTools } from "redux-devtools-extension";
    import { createReduxHistoryContext, reachify } from "redux-first-history";
    import { createWouterHook } from "redux-first-history/wouter";
    import { createBrowserHistory } from 'history';
    
    const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
      history: createBrowserHistory(),
    });
    
    export const store = createStore(
      combineReducers({
        router: routerReducer
        //... your reducers
      }),
      composeWithDevTools(
        applyMiddleware(routerMiddleware)
      )
    );
    
    export const history = createReduxHistory(store);
    
    // For @reach/router support
    export const reachHistory = reachify(history);
    
    // For wouter support
    export const wouterUseLocation = createWouterHook(history);
  5. Advanced: Configure React batch updates

    master

    To achieve maximum performance and prevent top-down React updates, you can provide a batch function (like unstable_batchedUpdates from react-dom) to the createReduxHistoryContext options.

    import { createReduxHistoryContext, reachify } from "redux-first-history";
    import { createBrowserHistory } from 'history';
    import { unstable_batchedUpdates } from 'react-dom';
    
    const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
      history: createBrowserHistory(),
      batch: unstable_batchedUpdates,
    });
  6. Advanced: Support @reach/router 'navigate'

    master

    To support imperative navigate calls from @reach/router, pass the globalHistory to the reachGlobalHistory option in createReduxHistoryContext.

    import { createReduxHistoryContext, reachify } from "redux-first-history";
    import { createBrowserHistory } from 'history';
    import { globalHistory } from '@reach/router';
    
    const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
      history: createBrowserHistory(),
      reachGlobalHistory: globalHistory,
    });
  7. Navigate programmatically using push()

    master

    You can trigger navigation from within Redux Sagas or Thunks using the push action creator.

    // In a Redux Saga
    import { put } from "redux-saga/effects";
    import { push } from "redux-first-history";
    
    function* randomFunction() {
      yield put(push("YOUR_ROUTE_PATH"));
    }
    
    // In a Redux Thunk (with @reduxjs/toolkit)
    import { push } from "redux-first-history";
    
    export const RandomThunk = (dispatch) => {
      dispatch(push("YOUR_ROUTE_PATH"));
    };
  8. Reference: createReduxHistoryContext options

    master

    The createReduxHistoryContext function accepts an options object to configure the router behavior.

    ```javascript
    export const createReduxHistoryContext = ({
      history, 
      routerReducerKey = 'router', 
      reduxTravelling = false, 
      selectRouterState = null,
      savePreviousLocations = 0,
      batch = null,
      reachGlobalHistory = null,
      basename
    })
    keyoptionaldescription
    historynoThe createBrowserHistory object - v4.x/v5.x
    routerReducerKeyyesif you don't like router name for reducer
    reduxTravellingyesif you want to play with redux-dev-tools
    selectRouterStateyescustom selector for router state. With redux-immutable state => state.get("router")
    savePreviousLocationsyesif > 0 add the key "previousLocation" to state.router, with the last N locations. [{location,action}, ...]
    batchyesa batch function for batching states updates with history updates. Usage: import { unstable_batchedUpdates } from 'react-dom';
    reachGlobalHistoryyesglobalHistory object from @reach/router to support imperative navigate. Usage: import { globalHistory } from '@reach/router';
    basenamenosupport basename (history v5 fix)
  9. Create router middleware with createRouterMiddleware

    master

    Use createRouterMiddleware to create a Redux middleware that synchronizes history changes with your Redux store. This middleware intercepts specific actions (of type CALL_HISTORY_METHOD) and executes the corresponding method on the provided history object (e.g., push, replace, go, etc.).

    Arguments

    ArgumentTypeDescription
    historyHistoryAn instance of the history package.
    showHistoryActionbooleanIf true, the middleware allows the history action to continue to the next middleware/reducer. If false, the action is swallowed after the history method is called.
    basename (optional)stringA prefix that will be automatically prepended to the first argument of push and replace calls.

    Usage Example

    import { createRouterMiddleware } from 'redux-first-history';
    import { createBrowserHistory } from 'history';
    
    const history = createBrowserHistory();
    
    const routerMiddleware = createRouterMiddleware({
      history,
      showHistoryAction: true,
      basename: '/app'
    });
    
    // Add routerMiddleware to your Redux store configuration
    export const createRouterMiddleware =
       ({ history, showHistoryAction, basename }: CreateRouterMiddlewareArgs): Middleware =>
       () =>
       // @ts-ignore
       (next: Dispatch) =>
       (action: ReduxAction) => {
          // ... implementation
       };
  10. Navigate using Redux action creators

    master

    Instead of calling the history object directly, you can dispatch Redux actions to trigger navigation. This ensures that the history state remains synchronized with your Redux store. The library provides several action creators that mirror the standard history API methods:

    • push(...args): Pushes a new entry onto the history stack.
    • replace(...args): Replaces the current entry on the history stack.
    • go(...args): Navigates to a specific index in the history stack.
    • goBack(): Navigates back one entry.
    • goForward(): Navigates forward one entry.
    • back(): Navigates back (compatible with history v5).
    • forward(): Navigates forward (compatible with history v5).

    These functions return a CallHistoryMethodAction which can be dispatched via dispatch().

    import { push, replace, goBack } from 'redux-first-history';
    
    // To navigate to a new URL:
    dispatch(push('/new-path'));
    
    // To replace the current URL:
    dispatch(replace('/other-path'));
    
    // To go back:
    dispatch(goBack());
  11. Integrate with Reach Router using reachify()

    master

    The reachify function is a utility that wraps a Redux-compatible history object (specifically one that includes a listenObject property) to make it compatible with the interface expected by Reach Router.

    It provides a ReachHistory object that includes:

    • navigate(to, options): A method to navigate to a new location. It returns a Promise that resolves when the transition is complete (via the internal _onTransitionComplete call).
    • listen(listener): A method to subscribe to history changes.
    • location: A getter for the current location.
    • transitioning: A boolean indicating if a navigation transition is currently in progress.
    • _onTransitionComplete(): An internal method used to signal the end of a transition, which resolves any pending navigation promises.
    import { reachify } from 'redux-first-history/reachify';
    // Assuming reduxHistory is your configured history object from redux-first-history
    const reachHistory = reachify(reduxHistory);
    
    // Use reachHistory with Reach Router