DEV Community

Cover image for Best Redux Architecture
Anatolii
Anatolii

Posted on

Best Redux Architecture

Intoduction

I want to warn people, which probably will remark about architecture, I absently appreciate your opinion, so if u find some remarks, just tell in comments, thanks.
Stack: React, NextJs, Typescript, Redux.

The ideology of this post isn't to write app, its about how powerful is redux with typescript in react of course, and we will use nextjs to write some example api requests.

So lets get started

First step is so simple

npx create-next-app --typescript
Enter fullscreen mode Exit fullscreen mode

So then we installing npm dependency

npm i redux react-redux redux-thunk reselect
Enter fullscreen mode Exit fullscreen mode

Also you can delete all usless files.

At first, add folder store in root folder and there create a file index.tsx, consequently folder modules and in this folder we creating another file index.ts, also here another folder with name App.

So store folder should look like that
Alt Text
After that, move to store/modules/App and creating base module structure:
index.ts, action.ts, enums.ts, hooks.ts, reducers.ts selectors.ts, types.ts
Alt Text

  1. enum.ts(for every new action u need new property in [enum]https://www.typescriptlang.org/docs/handbook/enums.html)
export enum TypeNames {
  HANDLE_CHANGE_EXAMPLE_STATUS = 'HANDLE_CHANGE_EXAMPLE_STATUS' 
}
Enter fullscreen mode Exit fullscreen mode

2.Then to make magic we need to install dev dependency -utility-types
types.ts - the imortant part

import { $Values } from 'utility-types';
import { TypeNames } from './enums';
Enter fullscreen mode Exit fullscreen mode

Just import TypeNames and $Values

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
};
Enter fullscreen mode Exit fullscreen mode

Describes which type have AppState

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS]: {
    isThisArchitecturePerfect: boolean;
  };
};
Enter fullscreen mode Exit fullscreen mode
export type ActionsValueTypes = {
  toChangeStatusOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS];
  };
};
Enter fullscreen mode Exit fullscreen mode

That's the code we need to tell our reducers which type of different actions we have.
specification* toChangeStatusOfExample can have just a random name, but I also give the identical name as (action function, but its a little bit soon)

export type AppActionTypes = $Values<ActionsValueTypes>
Enter fullscreen mode Exit fullscreen mode

In this step we need to make typescript magic, we will see soon, what magic I am telling.

So in result our types.ts file should look like that

import { $Values } from 'utility-types';
import { TypeNames } from './enums';

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
    isThisArchitecturePerfect: boolean;
  };
};

export type ActionsValueTypes = {
  toChangeStatusOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
  };
};
export type AppActionTypes = $Values<ActionsValueTypes>;

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
};
Enter fullscreen mode Exit fullscreen mode

You can presume that it is so bulky and over-coding, but if you appreciate your time its will give you the opportunity to save a lot of time in the future.

3.So next move to file reducers.ts

import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';
Enter fullscreen mode Exit fullscreen mode

As always at first we import modules.

const initialState: AppInitialStateType = {};
Enter fullscreen mode Exit fullscreen mode

Alt Text

Remarkably, as you see, it a typescript magic, because we have given to initialState the type AppInitialStateType where was describes that's const should have property isThisArchitecturePerfect, isThisArchitecturePerfect,
Alt Text
so when we will started to write something, we will again see the typescript magic.

Alt Text

Consequently, when we will start to write something, we will again see the typescript magic.

export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
  switch (action.type) {
    default:
      return state;
  }
}; 
Enter fullscreen mode Exit fullscreen mode

Pro temporary nothing special, just basic redux reducer with switch construction.

  1. In index.ts we just exporting our appReducer with default construction.
import { appReducer as app } from './reducers';
export default app;
Enter fullscreen mode Exit fullscreen mode

At least right now we should have something like that

//enum.ts**

export enum TypeNames {
  HANDLE_CHANGE_STATUS_OF_EXAMPLE = 'HANDLE_CHANGE_STATUS_OF_EXAMPLE',
}

//types.ts**

import { $Values } from 'utility-types';
import { TypeNames } from './enums';

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
    isThisArchitecturePerfect: boolean;
  };
};

export type ActionsValueTypes = {
  toChangeStatusOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
  };
};
export type AppActionTypes = $Values<ActionsValueTypes>;

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
}

//reducers.ts

import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';

const initialState: AppInitialStateType = {
  isThisArchitecturePerfect: true,
};
export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
  switch (action.type) {
    default:
      return state;
  }
}; 

//index.ts
import { appReducer as app } from './reducers';
export default app;

Enter fullscreen mode Exit fullscreen mode

So if yes, my congradulation, but what not all, then in store/modules/index.ts

export { default as app } from './App';
Enter fullscreen mode Exit fullscreen mode

This is a feature of es6 js.

And then we should connect it in store/index.ts by coding this :

import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import * as reducers from './modules';

const combinedRedusers = combineReducers({ ...reducers });
const configureStore = createStore(combinecRedusers, compose(applyMiddleware(thunkMiddleware)));

export default configureStore;
Enter fullscreen mode Exit fullscreen mode

* as reducers will import all reducers which you import in prev step, for sure we applying thunkMiddleware to async code. And exporting store of course.

After this, we need connect store to our pages/_app.tsx file, so we can do that by:

  1. Creating inlayouts folder StoreLayout, here create index.tsx which have <Provider store={store}>{children}</Provider>, I get sm like that:
import { FC } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import store from './../../store';

const StoreLayout: FC = ({ children }) => {
  return <ReduxProvider store={store}>{children}</ReduxProvider>;
};

export default StoreLayout;
Enter fullscreen mode Exit fullscreen mode

2.The main feature of layouts its that firstly we creating layouts/index.tsx file with this code:

import { FC } from 'react';

export const ComposeLayouts: FC<{ layouts: any[] }> = ({ layouts, children }) => {
  if (!layouts?.length) return children;

  return layouts.reverse().reduce((acc: any, Layout: any) => <Layout>{acc}</Layout>, children);
};
Enter fullscreen mode Exit fullscreen mode

The main idea isn't to have the nesting of your Providers because at least you will have a lot of different Providers. We can make it so simple withreduce().
And finally in pages/_app.tsx we need change default next code to our

import type { AppProps } from 'next/app';
import StoreLayout from '../layouts/StoreLayout';
import { ComposeLayouts } from '../layouts/index';

const _App = ({ Component, pageProps }: AppProps) => {
  const layouts = [StoreLayout];

  return (
    <ComposeLayouts layouts={layouts}>
      <Component {...pageProps} />
    </ComposeLayouts>
  );
};
export default _App;
Enter fullscreen mode Exit fullscreen mode

Of course, we want that our state isn't be static, so to do that we need to move to store/modules/App/action.ts and write simple action function, like that:

import { TypeNames } from './enums';
import { AppActionTypes, PayloadTypes } from './types';

export const toChangeThemePropertyies = (
  payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]
): AppActionTypes => ({
  type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,
  payload
});

Enter fullscreen mode Exit fullscreen mode

The important thing is to give payload(param of function) the correct type, so because we have enum TypeNames we cannot make mistakes with type naming. And the most impressive is that when we writing that this action should return AppActionTypes(its type with all actions type), and then writing in function type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE, payload will automatically be found. We will see the example soon.

Also having the opportunity, open store/modules/App/selectors.ts, there we use library reselect to have access to our state, main idea that if store chaging, and we using some value from store, component will rerender without reselect so, its so powerfull. But until we start creating reducers we need to have RootStoreType and I like to creating a new global folder models and here also create file types.ts
Alt Text and here:

import { AppInitialStateType } from '../store/modules/App/types';
export type RootStoreType = { app: AppInitialStateType };
Enter fullscreen mode Exit fullscreen mode

In this code, we should describe RootStoreType with all reducers. Now back to store/modules/App/selectors.ts

As always:

import { RootStoreType } from '../../../models/types';
import { createSelector } from 'reselect';
Enter fullscreen mode Exit fullscreen mode

Then good practice its starts naming your selector with `get

  • someName,like that: export const getIsThisArchitecturePerfect= createSelector() Also,createSelector` have 2 params:
  • Array with functions (in our case)(state:RootStoreType) =>state.app.isThisArchitecturePerfect
  • Function which takes in param (return values of prev Arr) and returning value which u need, Result code:
import { RootStoreType } from '../../../models/types';
import { createSelector } from 'reselect';

export const getIsThisArchitecturePerfect= createSelector(
  [(state: RootStoreType) => state.app.isThisArchitecturePerfect],
  isThisArchitecturePerfect => isThisArchitecturePerfect
);
Enter fullscreen mode Exit fullscreen mode

Finally, we can test does our logic work,to do that move to pages/index.tsx; and write this code:


import { useSelector } from 'react-redux';
import { getIsThisArchitecturePerfect } from '../store/modules/App/selectors';

const Index = () => {
  const isThisArchitecturePerfect = useSelector(getIsThisArchitecturePerfect);
  console.log(isThisArchitecturePerfect);
  return <></>;
};

export default Index;
Enter fullscreen mode Exit fullscreen mode

Where we import useSelector to get access to our store and paste to this how our selector, then due toconsole.log(isThisArchitecturePerfect) we will see the result.
So save all and run

npm run dev
Enter fullscreen mode Exit fullscreen mode

(F12 to open dev tools), I'm kidding because everybody knows that)
I think u we ask me, that our app is so static, and I will answer, yeah, and right now, will add some dynamic. Also to have better look, let's add simple stying and jsx markup and
we need a useDispatch() to change our store and imported our action function toChangeThemePropertyies, also let's create 2 functions to change value (first to true, second to false) like that:
Alt Text

as u see I especially, set 'true' not true, so this is typescript magic, u always know that your code work as you expect. I don't use CSS, because I so love to use JSS, because it have unbelievable functionality, and I have zero ideas why JSS is not so popular, but it's not about styling.

import { useDispatch, useSelector } from 'react-redux';
import { toChangeThemePropertyies } from '../store/modules/App/actions';
import { getIsThisArchitecturePerfect } from '../store/modules/App/selectors';

const Index = () => {
  const isThisArchitecturePerfect = useSelector(getIsThisArchitecturePerfect);
  const dispatch = useDispatch();

  const handleSetExampleStatusIsTrue = () => {
    dispatch(toChangeThemePropertyies({ isThisArchitecturePerfect: true }));
  };
  const handleSetExampleStatusIsFalse = () => {
    dispatch(toChangeThemePropertyies({ isThisArchitecturePerfect: false }));
  };

  const containerStyling = {
    width: 'calc(100vw + 2px)',
    margin: -10,
    height: '100vh',
    display: 'grid',
    placeItems: 'center',
    background: '#222222',
  };

  const textStyling = {
    color: 'white',
    fontFamily: 'Monospace',
  };

  const buttonContainerStyling = {
    display: 'flex',
    gap: 10,
    marginTop: 20,
    alignItems: 'center',
    justifyContent: 'center',
  };

  const buttonStyling = {
    ...textStyling,
    borderRadius: 8,
    cursor: 'pointer',
    border: '1px solid white',
    background: 'transparent',
    padding: '8px 42px',
    width: '50%',
    fontSize: 18,
    fontFamily: 'Monospace',
  };

  return (
    <>
      <div style={containerStyling}>
        <div>
          <h1 style={textStyling}>{'- Is This Architecture Perfect?'}</h1>
          <h1 style={textStyling}>{`- ${isThisArchitecturePerfect}`.toUpperCase()}</h1>
          <div style={buttonContainerStyling}>
            <button style={{ ...buttonStyling, textTransform: 'uppercase' }} onClick={handleSetExampleStatusIsTrue}>
              True
            </button>
            <button style={{ ...buttonStyling, textTransform: 'uppercase' }} onClick={handleSetExampleStatusIsFalse}>
              False
            </button>
          </div>
        </div>
      </div>
    </>
  );
};

export default Index;
Enter fullscreen mode Exit fullscreen mode

If you attentive, i guess u know why code don't work, so try to fix this small detail by yourself, if u don't wanna.
Solution that in store/modules/App/reducers.ts we forget to write case of our reducer switch construction so to fix that we need to write this

 case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE: {
      const { isThisArchitecturePerfect } = action.payload;
      return { ...state, isThisArchitecturePerfect };
    }
Enter fullscreen mode Exit fullscreen mode

and I have feature to improve this code to

//if your action.payload is the same as property in initial state u can write like this:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE1:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE2: ({ ...state, ...action.payload });
// if not, just create a new case

case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE: ({ ...state, ...action.payload });

Enter fullscreen mode Exit fullscreen mode

So right now all will be work correctly, but that not all, because as I said in the introduction we will write some simple api, so open or create pages/api and there create a file with your api route, in my case its pages/api/example, referring oficial docs

import type { NextApiRequest, NextApiResponse } from 'next';
import { ApiExampleResType } from '../../models/types';

export default (req: NextApiRequest, res: NextApiResponse<ApiExampleResType>) => {
  res.status(200).json({ title: '- Is This Architecture Perfect?' });
};
Enter fullscreen mode Exit fullscreen mode

yeah, and also in models/types.ts write type

 export type ApiExampleResType = { title: string }; 
Enter fullscreen mode Exit fullscreen mode

thats we need to 'typescript magic'. Then, we have some trobleness with due to nextjs getServerSideProps, so here we will simplify task, but at least u should use nextjs getServerSideProps in real app.

So task for you is creating your action function with payload type ApiExampleResType, just for training, if you are lazy, see result :

//enum.ts**

HANDLE_CHANGE_TITLE_OF_EXAMPLE ='HANDLE_CHANGE_TITLE_OF_EXAMPLE',  

//types.ts**

import { $Values } from 'utility-types';
import { TypeNames } from './enums';
import { ApiExampleResType } from './../../../models/types';

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
    isThisArchitecturePerfect: boolean;
  };
  [TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE]: ApiExampleResType;
};

export type ActionsValueTypes = {
  toChangeSphereCursorTitle: {
    type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
  };
  toChangeTitleOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE];
  };
};
export type AppActionTypes = $Values<ActionsValueTypes>;

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
} & ApiExampleResType;

//reducers.ts

import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';

const initialState: AppInitialStateType = {
  isThisArchitecturePerfect: true,
  title: 'Nothing',
};

export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
  switch (action.type) {
    case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE:
    case TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE:
      return { ...state, ...action.payload };

    default:
      return state;
  }
};

//action.ts

import { TypeNames } from './enums';
import { AppActionTypes, PayloadTypes } from './types';

export const toChangeThemePropertyies = (
  payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]
): AppActionTypes => ({
  type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,
  payload,
});

export const toChangeTitleOfExample = (
  payload: PayloadTypes[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE]
): AppActionTypes => ({
  type: TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE,
  payload,
});


Enter fullscreen mode Exit fullscreen mode

You have written the same, my congratulations), to have access to new property of our app state, we need to write a new selector, the next step is that in selectors.ts we adding this selector

export const getTitle= createSelector(
  [(state: RootStoreType) => state.app.title],
  title => title
);
Enter fullscreen mode Exit fullscreen mode

Penultimate step, is in opetations.ts
At first import all dependency

//types 
import { Action, ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { RootStoreType } from '../../../models/types';
import { AppActionTypes } from './types';
//action
import { toChangeTitleOfExample } from './actions';

Enter fullscreen mode Exit fullscreen mode

Secondary, created the thunk function with this typeActionCreator<ThunkAction<Promise<Action>, RootStoreType, void, any>> in which we have async closure with type
(dispatch: Dispatch<AppActionTypes>): Promise<Action> =>
in which we sending fetch get request, to our /api/example and return is dispatch(toChangeTitleOfExample(awaited result)). Probably a little bit bilky, but in result we have

import { Action, ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { RootStoreType } from '../../../models/types';
import { toChangeTitleOfExample } from './actions';
import { AppActionTypes } from './types';

export const operatoToSetExampleTitle:
  ActionCreator<ThunkAction<Promise<Action>, RootStoreType, void, any>> =
    () =>
      async (dispatch: Dispatch<AppActionTypes>): Promise<Action> => {
      const result = await fetch('/api/example', { method: 'GET' });
      const { title } = await result.json();
      return dispatch(toChangeTitleOfExample({ title }));
    };

Enter fullscreen mode Exit fullscreen mode

And the final step in pages/index.tsx:

  const title = useSelector(getTitle);

  useEffect(() => {
    dispatch(operatoToSetExampleTitle());
  }, []);

Enter fullscreen mode Exit fullscreen mode

Its no the best practice while we use nextjs, but just as example not the worst, useEffect(()=>{...},[]) - runs only on mount, so and hooks.ts we need to use while we have repeated logic in operations.ts or reducers.ts.

Conclusion

If you anyway think that is so bulky, I guarantee that this structure is awesome if you will just try to use, then you will not be able to use another architecture.

Thanks for reading, I so appreciate this ♥.

Source code (GitHub).

Discussion (4)

Collapse
markerikson profile image
Mark Erikson

Hi, I'm a Redux maintainer. Unfortunately, I have concerns with several of the things you've shown here in this post - the code patterns shown are the opposite of how we recommend people use Redux today.

You should be using our official Redux Toolkit package and following our guidelines for using Redux with TypeScript correctly. That will eliminate all of the hand-written action types, action creators, and a lot of the other code you've shown, as well as simplifying the store setup process. You're also splitting the code across multiple files by type, and we recommend keeping logic in a single-file "slice" per feature.

For example, this is what that "app slice" would look like with RTK + TS:

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

interface AppSliceState {
  isArchitecturePerfect: boolean;
}

const initialState : AppSliceState = {
  isArchitecturePerfect: false
}

const appSlice = createSlice({
  name: "app",
  initialState,
  reducers: {
    appImproved(state, action: PayloadAction<boolean>) {
      state.isArchitecturePerfect = action.payload;
    }
  }
})

export const { appImproved } = appSlice.actions;

export default appSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Notice how few TS typedefs are needed. We just need one for the initial state, and one for the payload of this action, and that's it. Everything else is inferred.

Note that we also recommend using pre-typed React-Redux hooks as well.

Please switch to using the patterns we show in the docs - it'll make everything much simpler!

Collapse
pas8 profile image
Anatolii Author

Thanks for your feedback, yeah, for sure to simplify code we should use redux-toolkit, but my idea was to show how we can manually set up with typescript our redux structure, just as an example how to have custom control at redux, probably or most likely its over-coding, I guess that for complex logic it has the right to exist due to a lot of error handlers, I am so sorry if somebody was misled due to title of the story. So should I delete the story or change totally sm?

Collapse
thexdev profile image
M. Akbar Nugroho

I would recommend RTK instead of creating my own architecture or folder structure (whatever you call it). RTK provides stardart way to manage states inside Redux. It's good for teamwork and easy to understand.

Collapse
fibo_52 profile image
Gianluca Casati

I started using React and Redux since 2015 everyday in production. I loved Redux. This year, in 2021 I changed my mind: now I do not use Redux anymore, I think that the effort done by the Redux Toolkit maintainers was huge but they overcomplicated the tool.

Now I go for the useReducer React hook, I think it is closer to the initial Redux idea that made me think in 2015 "how I love this tool, I am going to use it to build every webapp".

In Redux Toolkit, in particular typings is overcomplicated, I gave up trying to type a Middleware.

In general this is a topic related also to React itself, the more the API is simple clear and flexible, the more people will keep using React... please do not repeat the same error with Redux Toolkit that is so complicated and opinionated, Keep It Simple!

.... or you Redux Toolkit maintainers are still in time to trying to simplify it, come on! How can you create a TypeScript generic with more than 2 arguments!