I sometimes code applications using React and Vue.js. However, I’ve used Redux more than Vuex or Pinia, so I was wondering if it would be possible to use it in one of my projects with Nuxt 3. I came across some articles explaining how to use it with Vuejs 3, but I’m comfortable using Nuxt 3. So how can I use it with Nuxt 3 ?
I’ve created a todo app as an example. The link is at the end of this post.
Add Redux Toolkit to your project.
Just add @reduxjs/toolkit
to your Nuxt 3 project.
npm install @reduxjs/toolkit
Create a store and a Nuxt plugin
We need to create a store and a Nuxt plugin to use Redux Toolkit in Nuxt 3.
// src/store/store.ts
// ESM import Workaround with Redux Toolkit.
import * as reduxToolkit from "@reduxjs/toolkit";
import { PayloadAction } from "@reduxjs/toolkit";
const { configureStore, createSlice } = ((reduxToolkit as any).default ??
reduxToolkit) as typeof reduxToolkit;
export const todoSlice = createSlice({
name: "todos",
initialState: {
todoList: [] as Todo[],
},
reducers: {
addTodo: (state, action: PayloadAction<Todo>) => {
state.todoList.push(action.payload);
},
removeTodo: (state, action: PayloadAction<string>) => {
state.todoList = state.todoList.filter(
todo => todo.id !== action.payload
);
},
editTodo: (state, action: PayloadAction<Todo>) => {
state.todoList = state.todoList.map(todo =>
todo.id === action.payload.id ? action.payload : todo
);
},
},
});
export const { addTodo, removeTodo, editTodo } = todoSlice.actions;
export const store = configureStore({
reducer: {
todos: todoSlice.reducer,
},
});
type Todo = {
id: string;
label: string;
done: boolean;
};
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Let’s create a Nuxt plugin to provide the store to the Nuxt app.
import { EnhancedStore } from "@reduxjs/toolkit";
import { App, reactive } from "vue";
import { RootState, store } from "~/store/store";
export const storeKey = Symbol("Redux-Store");
export const createRedux = (store: EnhancedStore) => {
const rootStore = reactive<{ state: RootState }>({
state: store.getState(),
});
return {
install: (app: App) => {
app.provide<{ state: RootState }>(storeKey, rootStore);
store.subscribe(() => {
rootStore.state = store.getState();
});
},
};
};
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(createRedux(store));
});
Implementation of composables to use the store
We can implement the composables useDispatch
and useSelector
for the store.
// src/helpers/redux.ts
import { RootState, store } from "~/store/store";
import { storeKey } from "~/plugins/redux";
export const useDispatch = () => store.dispatch;
export const useSelector = <State extends RootState = RootState>(
fn: (state: State) => State[keyof State]
) => {
const rootStore = inject(storeKey) as { state: RootState };
return computed(() => fn(rootStore.state as State));
};
Usage in a component
Here is an example of how to use the store in a component.
//src/component/TodoForm.vue
<template>
<form @submit.prevent="onSubmit">
<h2 class="label-wrapper">
<label for="new-todo-input" class="label__lg">
What needs to be done?
</label>
</h2>
<input
type="text"
id="new-todo-input"
name="new-todo"
autocomplete="off"
v-model.trim="label"
class="input__lg"
/>
<button type="submit" class="btn btn__primary btn__lg">Add</button>
</form>
</template>
<script setup lang="ts">
import { useDispatch } from "~/helpers/redux";
import { addTodo as addTodoStore } from "~/store/store";
const dispatch = useDispatch();
const label = ref("");
function onSubmit() {
if (label.value === "") {
return;
}
dispatch(
addTodoStore({
label: label.value,
done: false,
id: crypto.randomUUID(),
})
);
label.value = "";
}
</script>
What about SSR ?
Nuxt 3 enables SSR (server-side rendering) by default. This can cause problems because the code is executed twice, which can cause hydration mismatch.
There are two solutions: either disable SSR in the Nuxt options (nuxt.config.ts), or wrap components that use the Redux toolkit with <LazyClientOnly></LazyClientOnly>
or <ClientOnly></ClientOnly>
.
I choose the second solution, because I want to keep SSR enabled.
// app.vue
<template>
<div id="app">
<h1>To-Do List</h1>
<todo-form />
<h2 id="list-summary" ref="listSummary" tabindex="-1">{{ listSummary }}</h2>
<LazyClientOnly>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in todos.todoList" :key="item.id">
<todo-item :label="item.label" :done="item.done" :id="item.id" />
</li>
</ul>
</LazyClientOnly>
</div>
</template>
//Rest of the code...