1. Frontend
  2. Store

Modular Store

There are 6 modules:

Theme

Stores a single value: user’s theme, which sets dark mode.

Pricing

Stores the selected product plan, billing period and currency, as well as the user’s current plan.

Auth

Stores an authenticated flag and the user’s JSON Web Token.

Account

Stores the user state (email, first name, last name…).

Tenant

Stores the current tenant and the current user workspace.

App

Stores the current plan features with their limits, and the current user usage.

Persited

Every store is saved in the client’s localStorage, using different methods for each framework/library.

Store

Vue2

Uses vuex and vuex-persistedstate.

store/index.ts
import Vue from "vue";
import Vuex, { StoreOptions } from "vuex";
import createLogger from "vuex/dist/logger";
import createPersistedState from "vuex-persistedstate";

// Modules
import { theme } from "./modules/themeState";
import { pricing } from "./modules/pricingState";
import { auth } from "./modules/authState";
import { app } from "./modules/appState";
import { account } from "./modules/accountState";
import { tenant } from "./modules/tenantState";
import { RootState } from "./types";

Vue.use(Vuex);

const debug = process.env.NODE_ENV !== "production";

const store: StoreOptions<RootState> = {
  modules: {
    account,
    auth,
    tenant,
    pricing,
    theme,
    app,
  },
  strict: debug,
  plugins: debug ? [createLogger(), createPersistedState()] : [createPersistedState()],
};

export default new Vuex.Store<RootState>(store);

Vue3

Uses the latest release of vuex and vuex-persistedstate.

store/index.ts
import { createStore, createLogger } from "vuex";
import createPersistedState from "vuex-persistedstate";

// Modules
import { account } from "./modules/accountState";
import { auth } from "./modules/authState";
import { tenant } from "./modules/tenantState";
import { pricing } from "./modules/pricingState";
import { theme } from "./modules/themeState";
import { app } from "./modules/appState";
import { RootState } from "@/store/types";

const debug = import.meta.env.NODE_ENV !== "production";

export const store = createStore<RootState>({
  modules: {
    account,
    auth,
    tenant,
    pricing,
    theme,
    app,
  },
  strict: debug,
  plugins: debug ? [createPersistedState(), createLogger()] : [createPersistedState()],
});

export default store;

React

Uses @reduxjs/toolkit to combine reducers.

store/index.ts
import { combineReducers, createStore } from "@reduxjs/toolkit";
import { loadState, saveState } from "./localStorage";
import themeReducer from "./modules/themeReducer";
import authReducer from "./modules/authReducer";
import throttle from "lodash.throttle";
import accountReducer from "./modules/accountReducer";
import tenantReducer from "./modules/tenantReducer";
import pricingReducer from "./modules/pricingReducer";
import appReducer from "./modules/appReducer";

const reducers = combineReducers({
  account: accountReducer,
  auth: authReducer,
  tenant: tenantReducer,
  pricing: pricingReducer,
  theme: themeReducer,
  app: appReducer,
});

const persistedState = loadState();
const store = createStore(reducers, persistedState);
store.subscribe(
  throttle(() => {
    saveState({
      account: store.getState().account,
      auth: store.getState().auth,
      tenant: store.getState().tenant,
      pricing: store.getState().pricing,
      theme: store.getState().theme,
      app: store.getState().app,
    });
  }, 1000)
);

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

Svelte

Uses svelte’s own store manager svelte/store.

store/index.ts
import { accountStore } from "./modules/accountStore";
import { appStore } from "./modules/appStore";
import { authStore } from "./modules/authStore";
import { pricingStore } from "./modules/pricingStore";
import { tenantStore } from "./modules/tenantStore";
import { themeStore } from "./modules/themeStore";

const store = {
  account: accountStore,
  auth: authStore,
  tenant: tenantStore,
  pricing: pricingStore,
  theme: themeStore,
  app: appStore,
};

export default store;

Theme State Samples

Vue2

State

store/modules/themeState.ts
import { Module } from "vuex";
import { Theme } from "@/application/enums/shared/Theme";
import { RootState, ThemeState } from "@/store/types";

const initialState: ThemeState = {
  theme: Theme.LIGHT,
};
export const theme: Module<ThemeState, RootState> = {
  namespaced: true,
  state: initialState,
  mutations: {
    reset(state: ThemeState) {
      state.theme = initialState.theme;
      const htmlClasses = document.querySelector("html")?.classList;
      htmlClasses?.remove("dark");
    },
    setTheme(state: ThemeState, payload: number) {
      state.theme = payload;
      const htmlClasses = document.querySelector("html")?.classList;
      if (payload === 0) {
        htmlClasses?.remove("dark");
      } else {
        htmlClasses?.add("dark");
      }
    },
  },
};

Usage

components/ui/toggles/DarkModeToggle.vue
<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
import store from "@/store";
import { Theme } from "@/application/enums/shared/Theme";

@Component({})
export default class DarkModeToggle extends Vue {
  toggle() {
    if (this.currentTheme === 1) {
      store.commit("theme/setTheme", Theme.LIGHT);
    } else {
      store.commit("theme/setTheme", Theme.DARK);
    }
    const htmlClasses = document.querySelector("html")?.classList;
    if (this.currentTheme === 0) {
      htmlClasses?.remove("dark");
    } else {
      htmlClasses?.add("dark");
    }
  }
  get currentTheme() {
    return store.state.theme.theme;
  }
}
</script>

Vue3

State

store/modules/themeState.ts
import { Module } from "vuex";
import { Theme } from "@/application/enums/shared/Theme";
import { RootState, ThemeState } from "@/store/types";

const initialState: ThemeState = {
  theme: Theme.LIGHT,
};
export const theme: Module<ThemeState, RootState> = {
  namespaced: true,
  state: initialState,
  mutations: {
    reset(state: ThemeState) {
      state.theme = initialState.theme;
      const htmlClasses = document.querySelector("html")?.classList;
      htmlClasses?.remove("dark");
    },
    setTheme(state: ThemeState, payload: number) {
      state.theme = payload;
      const htmlClasses = document.querySelector("html")?.classList;
      if (payload === 0) {
        htmlClasses?.remove("dark");
      } else {
        htmlClasses?.add("dark");
      }
    },
  },
};

Usage

components/ui/toggles/DarkModeToggle.vue
<script setup lang="ts">
import store from "@/store";
import { Theme } from "@/application/enums/shared/Theme";
import { computed } from "vue";

function toggle() {
  if (currentTheme.value === 1) {
    store.commit("theme/setTheme", Theme.LIGHT);
  } else {
    store.commit("theme/setTheme", Theme.DARK);
  }
  const htmlClasses = document.querySelector("html")?.classList;
  if (currentTheme.value === 0) {
    htmlClasses?.remove("dark");
  } else {
    htmlClasses?.add("dark");
  }
}
const currentTheme = computed(() => {
  return store.state.theme.theme;
})
</script>

React

store/modules/themeReducer.ts
import { Theme } from "@/application/enums/shared/Theme";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ThemeState } from "../types";

const value = JSON.stringify(localStorage.getItem("theme"));

const initialState: ThemeState = {
  value: value ? Number(value) : Theme.LIGHT,
};

export const themeSlice = createSlice({
  name: "theme",
  initialState,
  reducers: {
    hydrate: (state, action) => {
      // do not do state = action.payload it will not update the store
      return action.payload;
    },
    setTheme: (state, { payload }: PayloadAction<Theme>) => {
      state.value = payload;
      localStorage.setItem("theme", JSON.stringify(state.value));
    },
  },
});

export const { setTheme, hydrate } = themeSlice.actions;

export default themeSlice.reducer;

Usage

components/ui/toggles/DarkModeToggle.tsx
import { Theme } from "@/application/enums/shared/Theme";
import store, { RootState } from "@/store";
import { setTheme } from "@/store/modules/themeReducer";
import { useSelector } from "react-redux";

export default function Header() {
  const theme = useSelector<RootState>((state) => state.theme.value);

  const toggle = () => {
    const htmlClasses = document.querySelector("html")?.classList;
    if (theme === Theme.DARK) {
      store.dispatch(setTheme(Theme.LIGHT));
      htmlClasses?.remove("dark");
    } else {
      store.dispatch(setTheme(Theme.DARK));
      htmlClasses?.add("dark");
    }
  };
  return (
    ...

Svelte

State

store/modules/themeState.ts
import { writable } from "svelte/store";
import { Theme } from "../../application/enums/shared/Theme";
import type { ThemeState } from "../types";

const initialState: ThemeState = JSON.parse(localStorage.getItem("theme") ?? "{}") ?? {
  value: Theme.LIGHT,
};

export const themeState = writable(initialState);

export const themeStore = {
  setTheme: (value: Theme) =>
    themeState.update((self) => {
      self.value = value;
      return self;
    }),
};

themeState.subscribe((val) => {
  localStorage.setItem("theme", JSON.stringify(val));
});

Usage

components/ui/toggles/DarkModeToggle.svelte
<script lang="ts">
  import { Theme } from "@/application/enums/shared/Theme";
  import { themeState, themeStore } from "@/store/modules/themeStore";

  $: theme = $themeState.value;

  function toggle() {
    const htmlClasses = document.querySelector("html")?.classList;
    if (theme === Theme.DARK) {
      themeStore.setTheme(Theme.LIGHT);
      htmlClasses?.remove("dark");
    } else {
      themeStore.setTheme(Theme.DARK);
      htmlClasses?.add("dark");
    }
  }
</script>