Skip to content

Integrate Redux Toolkit in your Nuxt 3 app

Posted on:September 20, 2023 at 05:57 PM

Banner

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...

Resources