Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Follow publication

Syncing Tabs in Next.js 13+ using Redux Toolkit & Redux State Sync — Simple Guide

--

Syncing Tabs in Next.js 13+ using Redux Toolkit & Redux State Sync- Simple Guide
Syncing Tabs in Next.js 13+ using Redux Toolkit & Redux State Sync — Simple Guide

When working on a web app, particularly in highly dynamic apps like e-commerce we need to sync the states in different tabs. This article will discuss how to easily sync data between tabs using Redux State Sync in Next.js by building a simple counter app.

Without wasting our time let’s jump right into the tutorial:

1) Initialize your Next.js App:

To create a new Next.js Project run the following command:

npx create-next-app@latest

Choose the prompts accordingly:

What is your project named? next-with-pwa
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No

If you already have a Next project go to the next step. For further options visit the Official Website!

2) Add Redux Toolkit to Your Project:

If you already have redux setup move to the next step, else you can run the following command to install redux into your project.

npm install react-redux @reduxjs/toolkit

3) Add Redux-State-Sync and Redux-Persist to Your Project:

Run the following command in the terminal to install redux-state-sync to your project:

npm install redux-state-sync redux-persist

npm i --save-dev @types/redux-state-sync

4) Build a Counter Skeleton:

Now we will build a basic skeleton for our Counter App.

Update the code on app/page.tsx with the following code:

// app/page.tsx

import CounterComponent from "./components/counter";

export default function Home() {
return (
<main className="flex flex-col w-full h-[100vh] min-h-[400px] items-center justify-center gap-16">
<div className="text-center">
<h1 className="text-2xl font-bold leading-7 text-gray-900">
Tab Sync Example
</h1>
<p>Open this page in multiple tabs and see how the state is synced.</p>
</div>
<CounterComponent />
</main>
);
}

Update the app/global.css to remove any default styling:

// app/global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
padding: 0;
margin: 0;
box-sizing: border-box;
overflow-x: hidden;
}

Now add CounterComponent and ButtonComponent for our app:

// app/components/counter.tsx

import ButtonComponent from "./button";

const CounterComponent = () => {
return (
<div className="flex items-center justify-center gap-8">
<div className="border border-gray-700 px-4 py-1 rounded-md">0</div>
<ButtonComponent className="bg-green-400 hover:text-green-400 hover:border-green-400 hover:bg-transparent">
Add
</ButtonComponent>
<ButtonComponent className="bg-red-400 hover:text-red-400 hover:border-red-400 hover:bg-transparent">
Subtract
</ButtonComponent>
<ButtonComponent className="bg-blue-400 hover:text-blue-400 hover:border-blue-400 hover:bg-transparent">
Reset
</ButtonComponent>
</div>
);
};

export default CounterComponent;
// app/components/button.tsx

import { ButtonHTMLAttributes, DetailedHTMLProps, FC } from "react";

const ButtonComponent: FC<
DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
> = ({ children, className, ...props }) => {
return (
<button
className={`rounded-full py-1 px-5 transition-all text-white border-[2px] border-[color:transparent] ${className}`}
{...props}
>
{children}
</button>
);
};

export default ButtonComponent;

Here we have completed the basic structure for our app, and it will look as follows:

Example App UI
Example App UI

5) Configuring Redux in Our App:

To configure redux we will create two new folders in app directory with the name of redux and hooks.

First, we will add code for the store which is a central state of your app. Here we have added one slice and for now, we are leaving our middleware empty.

// redux/store.ts

"use client";

import { configureStore } from "@reduxjs/toolkit";

import counterSlice from "./counter.slice";

export const store = () => {
return configureStore({
reducer: {
counter: counterSlice,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(),
});
};

// just exported it as makeStore for the naming convention
export const makeStore = () => store;

// these exports are just intended to pass on the types
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

Now create a provider to wrap around our app to pass on the state to the client components.

// store/store-provider.tsx

'use client'

import { useRef } from 'react'
import { Provider } from 'react-redux'

import { makeStore, AppStore } from './store'

export default function StoreProvider({
children
}: {
children: React.ReactNode
}
) {
const storeRef = useRef<AppStore>()
if (!storeRef.current) {
storeRef.current = makeStore()
}

return <Provider store={storeRef.current}>{children}</Provider>
}

Now we will wrap this provider around our app in layout.tsx which goes as follows:

// app/layout.tsx

// ... other imports
import StoreProvider from "@/store/store-provider";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}
) {
return (
<html lang="en">
<body>
<StoreProvider>{children}</StoreProvider>
</body>
</html>
);
}

Now we will create a slice which is a collection of Redux reducer logic and provide actions and state for the single feature of the app.

// store/counter.slice.ts

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
count: 0,
};

const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
addCount: (state) => {
return {
...state,
count: state.count + 1,
};
},
subtractCount: (state) => {
if (state.count === 0) return state;
return {
...state,
count: state.count - 1,
};
},
resetCount: () => initialState,
},
});

export const { addCount, subtractCount, resetCount } = counterSlice.actions;

export default counterSlice.reducer;

Now we will add these actions and state them in our CounterComponent as follows:

// app/components/counter.tsx

"use client";

import { useAppDispatch, useAppSelector } from "@/hooks";

import { addCount, subtractCount, resetCount } from "@/redux/counter.slice";

import ButtonComponent from "./button";

const CounterComponent = () => {
const dispatch = useAppDispatch();
const { count } = useAppSelector((state) => state.counter);

return (
<div className="flex items-center justify-center gap-8">
<div className="border border-gray-700 px-4 py-1 rounded-md">{count}</div>
<ButtonComponent
className="bg-green-400 hover:text-green-400 hover:border-green-400 hover:bg-transparent"
onClick={() => dispatch(addCount())}
>
Add
</ButtonComponent>
<ButtonComponent
className="bg-red-400 hover:text-red-400 hover:border-red-400 hover:bg-transparent"
onClick={() => dispatch(subtractCount())}
>
Subtract
</ButtonComponent>
<ButtonComponent
className="bg-blue-400 hover:text-blue-400 hover:border-blue-400 hover:bg-transparent"
onClick={() => dispatch(resetCount())}
>
Reset
</ButtonComponent>
</div>
);
};

export default CounterComponent;

After doing this we have successfully configured Redux in our app and now the effect of buttons will look like as follows:

Working Redux Integration Demo
Working Redux Integration Demo

6) Configure Redux-Persist in Our App:

Here we need to use redux-persist to persist the state of our app on the frontend else it will throw us a hydration error:

First, we will update our store to include redux-persist as following:

// redux/store.ts

"use client";

import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";

import counterSlice from "./counter.slice";

const persistConfig = {
key: "root",
storage,
whitelist: ["counter"], // only this will be persisted
};

const rootReducer = combineReducers({ counter: counterSlice });

const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(),
});

export const persistor = persistStore(store);

// just exported it as makeStore for the naming convention
export const makeStore = () => store;

// these exports are just intended to pass on the types
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

Now we will create a provider to pass the context to our app:

// redux/persistor-provider.tsx

"use client";

import { PersistGate } from "redux-persist/integration/react";

import { persistor } from "./store";

export const PersisterProvider = ({
children,
}: {
children: React.ReactNode;
}
) => {
return (
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
);
};

In the end, we will wrap this provider at layout.tsx as follows:

// app/layout.tsx

// all other imports
import { PersisterProvider } from "@/redux/persistor-provider";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}
) {
return (
<html lang="en">
<body className={inter.className}>
<StoreProvider>
<PersisterProvider>{children}</PersisterProvider>
</StoreProvider>
</body>
</html>
);
}

7) Configure Redux-State-Sync in Our App:

Now it is straightforward to integrate redux-state-sync and we will also tweak the redux persist so that we do not get any errors:

// redux/store.ts

"use client";

import { configureStore, combineReducers } from "@reduxjs/toolkit";
import {
createStateSyncMiddleware,
initMessageListener,
} from "redux-state-sync";
import { persistStore, persistReducer } from "redux-persist";
import {
PERSIST,
PURGE,
REHYDRATE,
REGISTER,
FLUSH,
PAUSE,
} from "redux-persist/es/constants";
// if above es import is not working check -> "redux-persist/lib/constants"
import storage from "redux-persist/lib/storage";

import counterSlice from "./counter.slice";

const persistConfig = {
key: "root",
storage,
whitelist: ["counter"], // only this will be persisted
};

const rootReducer = combineReducers({ counter: counterSlice });

const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(
createStateSyncMiddleware({
predicate: (action) => {
const blacklist = [PERSIST, PURGE, REHYDRATE, REGISTER, FLUSH, PAUSE];
if (typeof action !== "function") {
if (Array.isArray(blacklist)) {
return blacklist.indexOf(action.type) < 0;
}
}
return false;
},
})
) as any,
});

initMessageListener(store);

export const persistor = persistStore(store);

// just exported it as makeStore for the naming convention
export const makeStore = () => store;

// these exports are just intended to pass on the types
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

After doing this, you will see something like that:

Tab Syncing Complete Example
Tab Syncing Complete Example

And that’s it, Happy Coding! 😊

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Written by Farasat Ali

Tech Savvy 💖 Blockchain, Web & Artificial Intelligence. Love to travel, explore culture & watch anime. Visit My Portfolio 👇 https://linktr.ee/faraasat

Responses (1)

Write a response