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
So then we installing npm dependency
npm i redux react-redux redux-thunk reselect
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
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
-
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'
}
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';
Just import TypeNames
and $Values
export type AppInitialStateType = {
isThisArchitecturePerfect: boolean;
};
Describes which type have AppState
export type PayloadTypes = {
[TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS]: {
isThisArchitecturePerfect: boolean;
};
};
export type ActionsValueTypes = {
toChangeStatusOfExample: {
type: typeof TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS;
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS];
};
};
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>
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;
};
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';
As always at first we import modules.
const initialState: AppInitialStateType = {};
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
,
so when we will started to write something, we will again see the typescript magic.
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;
}
};
Pro temporary nothing special, just basic redux reducer with switch construction.
- In
index.ts
we just exporting ourappReducer
withdefault
construction.
import { appReducer as app } from './reducers';
export default app;
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;
So if yes, my congradulation, but what not all, then in store/modules/index.ts
export { default as app } from './App';
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;
* 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:
- Creating in
layouts
folderStoreLayout
, here createindex.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;
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);
};
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;
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
});
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
and here:
import { AppInitialStateType } from '../store/modules/App/types';
export type RootStoreType = { app: AppInitialStateType };
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';
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
);
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;
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
(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:
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;
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 };
}
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 });
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?' });
};
yeah, and also in models/types.ts
write type
export type ApiExampleResType = { title: string };
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,
});
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
);
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';
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 }));
};
And the final step in pages/index.tsx
:
const title = useSelector(getTitle);
useEffect(() => {
dispatch(operatoToSetExampleTitle());
}, []);
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 ♥.
Top comments (5)
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:
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!
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?
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.
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!
React & Redux Application Architecture