Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 10, 2021 05:30 pm GMT

Modular Ducks - A design pattern for scalable redux architecture

I have worked with many different redux patterns and architectures, and I have found that none of them were perfectly ideal. The duck's pattern is prone to a circular dependency. The traditional folder-based approach requires you to separate action, reducers, selectors, etc into multiple files that can be cumbersome while making changes.
Redux toolkit provides an opinionated wrapper around redux and lets us do more with less code. But the issue with the Redux toolkit is that the project structure becomes similar to ducks and is prone to a circular dependency. Redux toolkit has already warned us of this issue here.
In this article, I am trying to explain my architecture with the Redux toolkit which is circular dependency safe, and also handles refactoring with ease.

Project Structure

Slices

  • Break your redux store based on the features of the app. With the Redux toolkit, we can use the createSlice API to create actions, reducers, and selectors for an individual slice.
  • One thing to keep in mind is no two slices should import from one another. There can be a case when we might have to trigger reducers in two slices based on one action. In that case, instead of importing action from one slice to another, create a common action using createAction and this action will be registered in both the slices withextraReducers.
  • Export the constant key from this file, to be used in `combine reducers to combine the reducers. Keeping the constant key in the slice file makes the store structure more predictable.
  • Keep all the selectors for keys of a slice file in their respective slice file. createGlobalStateSelector is a tiny npm library to generate global state selectors from the local slice selectors. This eases the refactoring effort by quite a lot.

Common Actions

  • Based on the project structure we can have multiple common action files.
  • Common action files should not import from any other file in the project directory.
  • Common actions can be used inside slices, thunks, and our components.

Common Selectors

  • Just like common actions, we might need selectors from different slices to combine them into one selector (e.g. createSelector from reselect).
  • Keeping combined selectors of two different slices outside the slice file in a different slice file, removes the circular dependency issue.
  • Common selectors file will import from slices file and will be used inside thunks and components.

Thunks

  • Thunk actions (or any redux middleware functions) should not be kept in a slice file. Thunks have access to the global state and it might have to dispatch action for some other slice.
  • You can create multiple files for thunk actions (it is always better to have multiple files than having one giant file). This can also be divided based on the features.
  • Thunk action files can import from slice files (actions and selectors), common action files, and common selectors.

Import diagram

redux import diagram

Sample Code

// personalDetailsSlice.jsimport { createSlice } from '@reduxjs/toolkit';import createGlobalStateSelector from 'create-global-state-selector';import { clearData } from './commonActions';export const sliceKey = 'personalDetails';const initialState = {  name: 'Ashish',  age: '26',  isEligibleToDrink: false};const { actions, reducer } = createSlice({  name: sliceKey,  initialState,  reducers: {    setName(state, { payload }) {      state.name = payload;    },    setAge(state, { payload }) {      state.age = payload;    },    setDrinkingEligibilityBasedOnAge(state) {      state.isEligibleToDrink = selectLocalAge(state) >= 18;    }  },  extraReducers: {    [clearData]: (state) => {      state.isEligibleToDrink = null;      state.age = null;      state.name = null;    }  }});function selectLocalName(state) {  return state.name;}function selectLocalAge(state) {  return state.age;}function selectLocalIsEligibleToDrink(state) {  return state.isEligibleToDrink;}export default reducer;export const { setName, setAge, setDrinkingEligibilityBasedOnAge } = actions;export const { selectName, selectAge, selectIsEligibleToDrink } = createGlobalStateSelector(  {    selectName: selectLocalName,    selectAge: selectLocalAge,    selectIsEligibleToDrink: selectLocalIsEligibleToDrink  },  sliceKey);
// educationalDetailsSlice.jsimport { createSlice } from '@reduxjs/toolkit';import createGlobalStateSelector from 'create-global-state-selector';import { clearData } from './commonActions';export const sliceKey = 'educationalDetails';const initialState = {  qualification: 'engineering'};const { actions, reducer } = createSlice({  name: sliceKey,  initialState,  reducers: {    setQualification(state, { payload }) {      state.qualification = payload;    }  },  extraReducers: {    [clearData]: (state) => {      state.qualification = null;    }  }});function selectLocalQualification(state) {  return state.qualification;}export default reducer;export const { setQualification } = actions;export const { selectQualification } = createGlobalStateSelector(  { selectQualification: selectLocalQualification },  sliceKey);
// commonActions.jsimport { createAction } from '@reduxjs/toolkit';export const clearData = createAction('detail/clear');
// commonSelectors.jsimport { createSelector } from '@reduxjs/toolkit';import { selectAge } from './personalDetailsSlice';import { selectQualification } from './educationalDetailsSlice';export const selectIsEligibleToWork = createSelector(  selectAge,  selectQualification,  (age, qualification) => age >= 18 && qualification === 'engineering');
// thunks.jsimport { fetchQualification } from './api';import { selectName } from './personalDetailsSlice';import { setQualification } from './educationalDetailsSlice';import { clearData } from './commonActions';export const getQualification = () => (dispatch, getState) => {  const state = getState();  const name = selectName(state);  fetchQualification(name)    .then(({ qualification }) => dispatch(setQualification(qualification)))    .catch(() => dispatch(clearData()));};
// store.jsimport { createStore, combineReducers } from 'redux';import personalDetailsReducer, { sliceKey as personalDetailsSliceKey } from './personalDetailsSlice';import educationalDetailsSlice, { sliceKey as educationalDetailsSliceKey } from './educationalDetailsSlice';const reducer = combineReducers({});const store = createStore(reducer);export default store;

The above example can scale well for large-scale projects. If you worried about the import rules, check out the Dependency cruiser

Do share with us your way of creating a modular and scalable redux structure.
If you're confused about anything related to this topic or have any questions. please comment below or reach out to me on Twitter @code_ashish.

Thanks For Reading


Original Link: https://dev.to/code_ashish/modular-ducks-a-design-pattern-for-scalable-redux-architecture-4dna

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To