Search code examples
reduxreact-reduxredux-sagaredux-toolkitrtk-query

Migrate to redux-toolkit for a big project


I have a react project with a huge codebase. I'm using redux, sagas, and async reducers. I have a redux-module structure. There are a lot of small reducers and I combine them into a few big async reducers. The same situation with sagas.

Is it possible to migrate to the redux-toolkit and rtk-query step by step? I can't rewrite all modules at one time. Maybe someone had experience doing the same migration? Or I should stay with my boilerplate codebase?)

Here is the code of one of async redux module

const reducer: (state: UserState, action: UserAction) => UserState = mergeReducers(
  userReducer,
  settingsReducer,
  signUpReducer
);

The mergeReducers function

const mergeReducers =
  (...reducers) =>
  (state, action) =>
    reducers.reduce((acc, reducer) => ({ ...acc, ...reducer(acc, action) }), state);

All of these reducers are standard reducers like

const signUpReducer = (state: UserState = userInitialState, action: SignUpAction): UserState => {
  switch (action.type) {
    case SIGN_UP_CONTINUE_REQUESTED:
    case SIGN_UP_INITIATE_REQUESTED:
      return { ...state, pending: true, error: null };
    case SIGN_UP_INITIATE_SUCCEEDED:
      return { ...state, pending: false };
    case SIGN_UP_CONTINUE_SUCCEEDED:
      return {
        ...state,
        profile: action.payload.profile,
        token: action.payload.token,
        error: null,
      };
    case SIGN_UP_INITIATE_REJECTED:
    case SIGN_UP_CONTINUE_REJECTED:
      return { ...state, pending: false, error: action.payload };

    default: {
      /* ::(action.type: empty) */
      return { ...state };
    }
  }
};

Implementation of modules here

function startSaga(key, saga) {
  const runnableSaga = function* main() {
    const sagaTask = yield fork(saga);
    const { payload } = yield take(STOP_SAGA);

    if (payload === key) {
      yield cancel(sagaTask);
    }
  };

  sagaMiddleware.run(runnableSaga);
}

function stopSaga(key) {
  store.dispatch({
    payload: key,
    type: STOP_SAGA,
  });
}

export const useReduxModule = (key, reducer, saga, initialAction) => {
  useEffect(() => {
    if (!store.asyncReducers[key]) {
      store.injectReducer(key, reducer);
      startSaga(key, saga);
      if (initialAction) initialAction();
    }

    return () => {
      stopSaga(key);
      store.removeReducer(key);
    };
  }, []);
};

Usage in react. I need to init redux module in the module root component

import { loadSomeDateRequested, reducer, saga } from './store';

const SomeComponent = ({ loadData }) => {
  useReduxModule(SOME_MODULE, reducer, saga, loadData);

  return (
    // some jsx
  );
};

export default connect(null, {
  loadData: loadSomeDateRequested,
})(SomeComponent);

SomeComponent.propTypes = {
  loadData: func.isRequired,
};

Store configuration

function createReducer(asyncReducers) {
  return combineReducers({
    ...staticReducers,
    ...asyncReducers,
  });
}

export const sagaMiddleware = createSagaMiddleware();

const bindMiddleware = (middlewares) =>
  (process.env.NODE_ENV !== 'production' && composeWithDevTools(applyMiddleware(...middlewares))) ||
  applyMiddleware(...middlewares);

export default function configureStore() {
  const store = createStore(createReducer(), bindMiddleware([sagaMiddleware]));
  store.asyncReducers = {};
  store.injectReducer = (key, asyncReducer) => {
    store.asyncReducers[key] = asyncReducer;
    store.replaceReducer(createReducer(store.asyncReducers));
  };
  store.removeReducer = (key) => {
    delete store.asyncReducers[key];
    delete store.getState()[key];
  };
  return store;
}

export const store = configureStore();

Static reducer is

export default {
  [MODULE_PDF_MODAL]: pdfModalReducer,
};

I spent a lot of time researching and reading docs and examples. But I didn't find an example of migration for the real projects only examples of how to migrate the simplest redux store. Maybe someone knows how to add a redux toolkit and keep the existing store working. Because for now, I know only one solution. And this solution is to rewrite all redux stores at one time. As I wrote upper I have a few async modules and they are independent of each other. It's realistic to migrate by modules but I need to keep all other's workings before I'll rewrite them.

Thank you so much for all your answers. Hope someone can help me)

Solution

import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import staticReducers from '@sharedStore/staticReducers';
import { combineReducers } from 'redux';

function createReducer(asyncReducers) {
  return combineReducers({
    ...staticReducers,
    ...asyncReducers,
  });
}

export const sagaMiddleware = createSagaMiddleware();

export default function configStore() {
  const store = configureStore({
    reducer: createReducer(),
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(sagaMiddleware),
  });
  store.asyncReducers = {};
  store.injectReducer = (key, asyncReducer) => {
    store.asyncReducers[key] = asyncReducer;
    store.replaceReducer(createReducer(store.asyncReducers));
  };
  store.removeReducer = (key) => {
    delete store.asyncReducers[key];
    delete store.getState()[key];
  };
  return store;
}

export const store = configStore();

You can use redux-toolkit and keep redux and it works stable. For me, it's for support combineReducers func from redux package.


Solution

  • Redux Toolkit is just Redux. A RTK reducer created by createSlice or createReducer is no different to "the outside" than a reducer written by you by hand. That means you can do whatever you want with them, even stuff like putting them into your mergeReducers.

    Our advice: switch your createStore over to configureStore. That will already add some helper middleware that your whole store will profit from - immutability and serializability checks and that stuff. And then just switch the reducers you have slowly over to RTK - just the ones you need to touch anyways. And over time, your project will migrate.

    As for going to RTK Query - that is just a change of paradigm. Of course you can also do this over time, but there is not a lot of similarity between reducer+saga and hooks+RTKQ - you'll have to decide for yourself on how to approach that.