State is a crucial part of any application as it handles how data is passed within the application itself. The management of state becomes complex as applications become larger, necessitating the use of a central repository for accessing application data. This central repository helps ensure that state management in the application is simple, atomic, and extendible.
In this article, we’ll take a closer look at and compare two of the most popular state management libraries available in the frontend ecosystem today: Redux and Vuex.
State management is the method of reading and changing state (i.e., data) that is shared between various components of an application.
State is not always complex. In some cases, state can be small and easily managed locally. For example, it would be pretty straightforward to handle passing data from a parent component A to a child component B. But, what if there was a need for a piece of information from component A in a foreign component J?
One way to manage state, in this case, might be to propagate the data. However, this is not always an efficient practice.
As mentioned earlier, when applications become larger, data management tends to become more complex. This drives the need for a centralized repository for storing state that can be used across various components (even unrelated components), as opposed to prop-drilling, which can become a pain in the neck for deeply nested components.
This precipitates the need for a global state, a central source of truth where all application components can have easy access to all application state, with little to no restrictions.
Since its creation in 2015, Redux has grown to be the most popular state-management library for React app development. It is a predictable state container that lets us use plain JavaScript while enforcing consistent patterns that ensure our app is reliable across client, server, and native environments and is also easy to test.
In addition to React, Redux can be used with any other JavaScript framework or library, such as Vue.js. However, Redux has received some criticism lately claiming that it is difficult to use. This negative feedback probably came about because writing actions to define every possible state change and then creating multiple reducers to control those actions can result in a lot of code, which can quickly become hard to manage.
To solve this problem, Dan Abramov and Andrew Clark, software engineers at Facebook and React core team members, created Redux Toolkit. It simplifies the store setup and reduces the required boilerplate, making it the go-to tool for efficient Redux development. Redux Toolkit also follows best practices by default.
Before we look at how Redux is used to manage state in a React application, let’s find out how it works.
A typical Redux setup is comprised of the following:
Here’s a diagram from redux.js.org showing what the flow of data looks like in the Redux state management library:
Let’s take a closer look at how Redux is used for managing application-wide state in a React application. For this demonstration, we’ll keep things simple by building a basic counter app to show implementation.
To start, create a React project and name it redux-demo
, using the below command:
create-react-app redux-demo
Now, install the redux
package (this will enable us to use Redux in the application) and another package called react-redux
that makes connecting React applications to Redux stores and reducers very simple:
npm install redux react-redux >
Next, start the server:
npm start
Since we’re focusing on using Redux, we won’t go through the details of building React components.
In the src
folder, create a new folder called components
, then create two files called Counter.js
and Counter.css
. Add the following code to the Counter.js
file:
const Counter = () => { return ( <main> <h1>Redux Counter</h1> <div>{counter}</div> <button>Increment Counter</button> <button>Decrement Counter</button> </main> ); }; export default Counter;
Here, we have two buttons that we’ll later use to increment or decrement the counter
value.
Next, import Counter.js
into App.js
. The updated App.js
file should look like this:
import Counter from './components/Counter'; function App() { return <Counter />; } export default App;
In the src
folder, create another folder called store
; within this folder create a file called index.js
. We will not subscribe to the store yet, since we are just setting up the counter
value; we are not ready to start listening for changes in its state.
Next, connect the app to the Redux store, so that components within the app can use the store; and export an instance of the counterReducer
.
The src/store/index.js
file should now look like this:
import { createStore } from "redux"; const counterReducer = (state = { counter: 0 }, action) => { if (action.type === "increment") { return { counter: state.counter + 1, }; } if (action.type === "decrement") { return { counter: state.counter - 1, }; } return state }; const store = createStore(counterReducer); export default store;
In order for the app components to use the store as a central state repository, we must wrap the root App
component with the Provider
component, which we import from react-redux
. Then, we’ll make reference to the store by passing it as a prop to the Provider
component.
To achieve this, update the src/index.js
file, like so:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import { Provider } from 'react-redux'; import store from './store/index' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> );
To get access to the data in the store from the Counter
component, we’ll import the useSelector
Hook from the react-redux
library. This Hook will enable us to make use of the specific pieces of the store that we wish to use.
Another benefit of useSelector
is that it manages subscriptions behind the scenes.
We’ll use the useSelector
Hook to extract the counter
state from the store. Next, we’ll pass a function that receives the state that is managed by Redux and the part of the state that we want to extract. Then, we’ll output the counter
value, as shown below:
import { useSelector } from 'react-redux'; const Counter = () => { const counter = useSelector((state) => state.counter); return ( <main> <h1>Redux Counter</h1> <div>{counter}</div> <button>Increment Counter</button> <button>Decrement Counter</button> </main> ); }; export default Counter;
So far, we’ve seen how we can retrieve data that is managed by Redux. Now, let’s see how we can update data.
To increment or decrement the counter
value by 1
, we’ll make use of the useDispatch
Hook from the react-redux
package.
First, we’ll call the useDispatch
, which gives us a dispatch function that we can call to dispatch the actions against the Redux store. Next, we’ll create methods that we can call using the various buttons. We’ll also pass the type identifiers.
The updated Counter.js
file looks like this:
import { useSelector, useDispatch } from 'react-redux'; const Counter = () => { const counter = useSelector((state) => state.counter); const dispatch = useDispatch(); const incrementHandler = () => { dispatch({ type: 'increment' }); }; const decrementHandler = () => { dispatch({ type: 'decrement' }); }; return ( <main> <h1>Redux Counter</h1> <div>{counter}</div> <button onClick={incrementHandler}>Increment Counter</button> <button onClick={decrementHandler}>Increment Counter</button> </main> ); }; export default Counter;
Now, when either the increment
or decrement
button is clicked, appropriate actions take place to either increase or decrease the counter
value by 1
.
We’ve had a close look at how Redux is used to manage state in a React app. Now, let’s see how Vuex can be used to manage state within a Vue application.
Vue was created by Evan You and is maintained by the Vue Core Team. Vuex is based on the same flux architecture as Redux. Vuex is a state management pattern and library for Vue.js applications.
With this library, application state is centralized so that every component in an application has access to the state the app needs at any point in time. With Vuex, getting state is made easy, and changing state is purposeful.
Vuex is a collection of states, getters, mutations, and actions:
Here’s a diagram from vuex.vuejs.org, illustrating what the flow of data looks like in the Vuex state management library:
To show how Vuex is used to manage state in Vue applications, let’s build a replica of the counter app from the Redux demo, with the same structure and functionality.
Start by creating the Vue project name vuex-demo
, using the below command:
vue create vuex-demo
During the Vue project setup, we’ll be asked a series of questions. For this tutorial, let’s go with the following configurations:
Prompt | Option |
---|---|
Please pick a preset | Manually select features |
Check the features needed for your project | All pre-selected options + Vuex |
Choose a version of Vue.js that you want to start the project with | 3.x |
Pick a linter / formatter config | ESLint with error prevention only |
Pick additional lint features | Lint on save |
Where would you prefer placing config for Babel, ESLint, etc. | In dedicated config files |
Save this as a preset for future projects? | N |
Notice how easy it was to install Vuex into our application right from its creation, which can be found by navigating to src/store/index
.
If an app doesn’t have Vuex, simply install the library using the following command:
npm install vuex@next --save
N.B., adding @next in the above code installs the latest version of Vuex
Moving on, since we are focusing on using Vuex, we’ll keep the Counter
components simple.
Navigate to the src
folder, create a file called Counter.vue
, and add the following code:
<template> <main> <h1>Vuex Counter</h1> <div>0</div> <button>Increment Counter</button> <button>Decrement Counter</button> </main> </template>
Next, import Counter.vue
into the App.vue
file. The App.vue
file should look like this:
<template> <Counter /> </template> <script> import Counter from './components/Counter.vue'; export default { name: 'App', components: { Counter } } </script> export default App;
Next, we need to create an appropriate state, mutations, actions, and getters for the demo.
State is a bag of properties in the store. So, to define a state for the counter
value, we insert it as a key value to the state object in the store. We also give it an initial value of 0
, as shown below:
import { createStore } from 'vuex'; export default createStore({ state: { counter: 0, }, mutations: {}, actions: {}, modules: {}, });
To access the counter
value in the Counter
component, we first import the store, then target the required state (in this case, counter
):
<template> <main> <h1>Vuex Counter</h1> <div>{{counter}}</div> <button>Increment Counter</button> <button>Decrement Counter</button> </main> </template> <script> import { computed } from '@vue/reactivity'; import store from '../store'; export default { setup() { const counter = computed(() => { return store.state.counter }) return { counter }; }, }; </script>
In order to make the buttons update the counter
value, we’ll create methods in the Counter
component and dispatch an action that calls the mutation (where we pass a payload) for updating the state. It is the payload passed from the Counter
component that is used to update the counter
state (decrementing or incrementing it by 1
).
To achieve this, we’ll update Counter.vue
, like so:
<template> <main> <h1>Vuex Counter</h1> <div>{{ counter }}</div> <button @click="increment">Increment Counter</button> <button @click="decrement">Decrement Counter</button> </main> </template> <script> import { computed } from '@vue/reactivity'; import store from '../store'; export default { setup() { const counter = computed(() => { return store.state.counter }) const increment = () => { store.commit('increment', { value: 1 }) } const decrement = () => { store.commit('decrement', { value: 1 }) } return { counter }; }, }; </script>
Next, update the store:
import { createStore } from 'vuex'; export default createStore({ state: { counter: 0, }, mutations: { increment(state, payload) { state.counter = state.counter + payload.value; }, decrement(state, payload) { state.counter = state.counter - payload.value; }, }, actions: {}, modules: {}, });
Suppose we want to read the value of a state and perhaps use it to perform an external operation. We can achieve that using getters.
In the Counter demo, we’ll read the counter
value and render that value multiplied by 2
on the DOM.
Let’s update the store to use getters, like this:
import { createStore } from 'vuex'; export default createStore({ state: { counter: 0, }, mutations: { increment(state, payload) { state.counter = state.counter + payload.value; }, decrement(state, payload) { state.counter = state.counter - payload.value; }, }, actions: {}, getters: { modifiedCounterValue(state) { return state.counter * 2; }, }, modules: {}, });
We can now access the modifiedCounterValue
from the App.vue
file. We achieve that using a computed
property, as shown below:
<template> <Counter /> <div>Modified counter value: {{modifiedCounterValue}}</div> </template> <script> import { computed } from '@vue/reactivity'; import Counter from './components/Counter.vue'; import store from './store'; export default { name: 'App', components: { Counter, }, setup() { const modifiedCounterValue = computed(() => { return store.getters.modifiedCounterValue }) return { modifiedCounterValue, } } } </script>
In Vue, it’s best practice to use actions only while performing asynchronous operations. Since our little demo is not asynchronous, we can lean into the Vuex docs to learn about how to use actions to dispatch data while working with asynchronous operations and handling promises, such as fetching data from an API.
The Redux and Vuex state management libraries that both widely used in the developer ecosystem. In this article, we explored both libraries, showed how they work, and demonstrated how to use them. We also looked at the Redux Toolkit and showed how it helps simplify Redux setup, helps avoid common bugs, and replaces the createStore
with an improved version called configureStore
.
You can read more about the Redux Toolkit in the official docs. Likewise, you can learn more about Vuex from its official documentation.
These libraries can serve as a predictable, central store for all application state, but it is best to avoid using them with applications that are not very complex as this could become a tedious and cumbersome approach.
Redux is a more popular and better-supported library compared to Vuex, but Vuex seems to be a better state management library for maintaining performance. However, the choice is yours and depends on your specific project needs.
I hope this article has helped you better understand how both Redux and Vuex work and has provided you with useful information to determine which solution is best for your project.
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.