Intro
One year ago, I started to learn Redux for the first time. I was a code newbie, and I had a lot of difficulties understanding Redux! I was annoyed that I couldn't pass props from child components to the parent components in React, and I really needed Redux for that!
I searched for alternatives to use instead of Redux, like React Context APIs, and I was happy with it because it’s already part of React. I didn't want to install any additional packages, and I found it easier to understand than Redux!
In my first big project with React, I used React Context APIs and the project was so big and complex! After a while, the project became increasingly complex until I lost control, and couldn’t even make small changes. Plus, the states in React Context APIs were really a mess!
What is Redux
Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time-traveling debugger.
Why do we need a Global store?
In all major frontend frameworks like Angular, React, Vue, & Svelte, you can only pass data between your components from the parent components to the child components, and it is super hard to pass the data upward to the parent components! So Redux helps us to share the data easily with our different components!
Redux’s problem
But the real problem with Redux is organizing your files! Most of the tutorials just show you how you can configure a store, create a reducer, use dispatch, …etc, but after you use Redux for a while, you will have a mess of unorganized files, and you will need to learn how to organize the Redux files in your project! In this article, we will learn about the most used Redux patterns in real-world applications!
Also in this tutorial, we will apply all of these patterns to this simple app! It’s a counter app created with create-react-app
Redux Tool Kit (Recommended)
This is the recommended pattern by the Redux core team, RTK is so much simpler and easier than using Redux! It uses slices, a new way to create and write the reducers, initial state and actions.
Each slice file follows this pattern:
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
And finally, we connect the slices with each other in the store.js
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../path/to/usersSlice'
import postsReducer from '../path/to/postsSlice'
import commentsReducer from '../path/to/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
Here is live example, run npm run dev
https://stackblitz.com/edit/vitejs-vite-yaxe7w?ctl=1&embed=1&file=src/App.jsx&hideNavigation=1
Then you’ll use this store for the Redux provider in your app, simple isn’t it?
For more info about this pattern take a look at Redux Essentials, Part 2: Redux App Structure
Pros:
As you see, everything is super organized and easy to edit and read!
Redux ducks
Redux Ducks is a different design pattern created by Erik Rasmussen, and in this pattern, we are merging all of our single reducer files into the reducer file itself, let’s see:
redux/
ducks/
counter.js
// Put all of our reducers here
store.js
And every reducer (duck file) will be:
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";
const initialState = {
count: 0,
};
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
}
export const increment = () => ({
type: INCREMENT,
});
export const decrement = () => ({
type: DECREMENT,
});
As you can see, we start with declaring our action types, and have no need to export them:
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";
Then the reducer (MUST BE DEFAULT EXPORTED).
const initialState = {
count: 0,
};
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
}
Finally, create the action types
export const increment = () => ({
type: INCREMENT,
});
export const decrement = () => ({
type: DECREMENT,
});
And that’s it! We have a standalone reducer that is so much easier to work with!
Other strategies
There are some software engineers who like to put the reducer next to the files or components that they will use in them:
app/
modules/
user/
__tests__/
user.reducer.spec.js
components/
user.reducer.js <- the user reducer
course/
__tests__/
course.reducer.spec.js
components/
course.reducer.js <- the course reducer
lesson/
__tests__/
lesson.reducer.spec.js
components/
lesson.reducer.js <- the lesson reducer
store.js
index.js
The old pattern
This pattern is used by old apps, it’s not recommended to use it but you may find it when you work on old projects
redux/
types/
counter.js
actions/
counter.js
reducers/
counter.js
store.js
It is a folder called reducer
that contains three other folders. and the store.js
file in the root! Let's break them down.
redux/types/counter.js
is where we will save the action type as shown below:
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
We added two action types for the two operations that we want to create, increment, and decrement.
redux/actions/counter.js
here we will add all of our action functions for the counter:
import { INCREMENT, DECREMENT } from "../types/counter";
export const increment = () => ({
type: INCREMENT,
});
export const decrement = () => ({
type: DECREMENT,
});
First, we imported the types from the types file. Then, we created the two functions of our counter and exported them:
redux/reducers/counter.js
is the reducer for our counter.
import { INCREMENT, DECREMENT } from "../types/counter";
const initialState = {
count: 0,
};
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
}
As you see, we imported the counter types again, and created our reducer.
Finally, redux/store.js
is where we will combine all of our reducers, and then configure our store:
import { createStore, combineReducers } from "redux";
import counterReducer from "./reducers/counter";
const reducer = combineReducers({
counter: counterReducer,
});
const store = createStore(reducer);
export default store;
We imported the reducers first! Then we combined reducers, and finally create the store!
Pros:
As you see, everything is super organized, and easy to edit and read!
Cons:
Every reducer needs three files to be created, and this will end up with a massive number of files.
Finally
As you see, there are a lot of cool ideas to keep your Redux file organized. Try them, and choose the pattern that fits your project! And always work to keep your Redux files organized because this would be a game-changer for your project. 😉