Redux (Redux Toolkit) [ in React vs NextJS ]

React Redux Toolkit
React Redux Toolkit

What Pain does redux solves

  • Redux is a global state.
  • Redux may not be necessary for every project.
  • You may need Redux if you don’t want to do props drilling (i.e. passing props too deep).
  • If you are still confused about Redux, just think about the React state. The only difference is you can access the state from anywhere.

In complex web applications like Facebook and Instagram, managing data and state across different components is crucial. Without a proper mechanism in place, the application can quickly become chaotic. You can think of Redux as a frontend database that helps keep everything organized.

Project Structure:

src
- redux
	- slices
		- anyname.js
	- store.js

Rules:

  • One application cannot have more than one store.
  • Action, Reducers & Subscriptions can be upto any number.

Main Processes in Redux

Actions: Actions are plain JavaScript objects that represent an intention to change the state. Action’s purpose is to signal that something happened in the application. For Ex: Think of them as “messages” sent to the store — indicating that a state change should occur.

Reducers: Specify how the state changes in response to actions. Reducers are pure functions that take the current state and an action as arguments, and return a new state.

Store: Store is a central object that holds the entire state of your application.

Subscriptions: When a Component subscribes to the store, it start to receive updates. When anything changes in the state, the components are notified, if its new state, it will re-render the UI.

REDUX 1ST TIME SETUP

Step 1: Install 2 Packages

npm install react-redux @reduxjs/toolkit

Step 2: CREATE THIS Structure

Create a ‘redux’ folder and create structure like this:

redux
	- store.js
	- slices
		- slice1.js

If you are using React, then create like this in src folder.

If you are using Next, then create like this in src/app folder.

Step 3: Create a Global Store

redux
	- store.js

// store.js (put these code)

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: { },
})

Step 4: Wrap your whole app with Redux Provider

// app/layout.js (Next)

// src/main.jsx (React)

(providing store to our complete react app). wrap your whole app inside provider. wrapping will give access all your files to react redux

in React

import {Provider} from "react-redux"
import {store} from "./redux/store.js"
const root= ReactDOM.createRoot(document.getElementById("root"))
root.render(
<Provider store={store}>
    <App />
</Provider>
)

in NextJS

In Next.js App Router, there is no need for ReactDOM.createRoot or root.render. Instead, you wrap your entire app with the Redux Provider directly inside the layout.js file.

// app/layout.js
"use client"; // This component must be client-side

import { Provider } from "react-redux";
import { store } from "../redux/store"; // Adjust path if necessary
import "./globals.css";

export const metadata = {
  title: "Next.js App with Redux",
  description: "Using Redux with the App Router",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Provider store={store}>{children}</Provider>
      </body>
    </html>
  );
}

If you do like this, there will be error of use client on importing store and metadata.

#ERROR

Solution: Split Client and Server Logic

We can handle this issue by splitting the Redux logic into a separate client component while keeping the RootLayout as a server component.

Step-by-Step Fix

  1. Create a ReduxProvider Component for Client Logic

create this Provider as component in components folder

// components/ReduxProvider.js

"use client"


import { Provider } from "react-redux";
import { store } from "../redux/store";

export default function ReduxProvider({ children }) {
return <Provider store={store}>{children}</Provider>;
}
  1. Update the Layout Without use client Directive
    This layout will remain a server component, allowing the metadata export.
// app/layout.js
import ReduxProvider from "../components/ReduxProvider";

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ReduxProvider>{children}</ReduxProvider>
</body>
</html>
);
}

NOW REAL WORK STARTS:

Step 1: Create a Slice

redux folder > slices folder > new slice

//slice1.js

A Slice takes 3 things:

  • name
  • inital state
  • reducers
    • actions
    • action creators
import { createSlice } from "@reduxjs/toolkit"

export const cartSlice = createSlice({

	name: 'counter',

	initialState: {

		value: 0

	},

	reducers: {

		increment: (state) => { // Actions

			state.value += 1 // Reducers

		},

		decrement: (state) => {

			state.value -= 1

		},

		incrementByAmount: (state, action) => {

			state.value += action.payload

		},

	}

})

export const {
	increment,
	decrement
} = cartSlice.actions //exporting actions

export default cartSlice.reducer // exporting reducer

Step 2: (store.js) Add the Slice in store.js

redux folder > store.js

This is your global/redux store

we will make

src
- redux
	- store.js
import { configureStore } from '@reduxjs/toolkit'

import cartSlice from './slices/cartSlice'

const store = configureStore({

  reducer: {

cart: cartSlice,    

//  "slice name" : "slice function"

},

})

export default store

Step 3: UI using — useDispatch & useSelector

// app/page.js (in Next)

// src/app.jsx (in React)

using useSelector and useDispatch in react components

'use client'
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from "../redux/slices/counterSlice";

export default function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()

  return (
    < >
        <button aria-label="Increment value" 
onClick={() => dispatch(increment())} >
          Increment
        </button>
        <span>{count}</span>
        <button aria-label="Decrement value" 
onClick={() => dispatch(decrement())} >
          Decrement
        </button>
    < />
  )
}

For React, it’s same as above.

For Next JS, make this client component by ‘use client’

Note: wherever you use useDispatch or useSelector, don’t forget to put ‘use client’.

# (useDispatch) Send Redux Action

Normal useDispatch

import { useDispatch} from 'react-redux'

import { decrement, increment } from './counterSlice'

export default function Counter() {

  const dispatch = useDispatch()

  return (

    < >

        <button aria-label="Increment value" onClick={() => dispatch(increment())} >

          Increment

        </button>

        <button aria-label="Decrement value" onClick={() => dispatch(decrement())} >

          Decrement

        </button>

    < />

)}

Action with Payload

Dispatch with payload

import { useDispatch} from 'react-redux'

import { decrement, increment } from './counterSlice'

export default function Counter() {

  const dispatch = useDispatch()

return(..........)

    <button id="btnDecrement" onClick={btnDecre}> - </button>

function btnDecre(){

    dispatch(btnDecrement())

}

directly: onClick={() => dispatch(decrement())}

# (useSelector) Use Redux State

import { useSelector } from 'react-redux'

export default function Counter() {

  const count = useSelector((state) => state.counter.value)

  return (

    < >
        <span>{count}</span>
    < />

  )
  • state.counter.value :
    • state – global state
    • counter – slice name
    • value – variable name
export const cartSlice= createSlice({

name: 'counter',

initialState: {

value:0

},

ERROR NOTES:

Error 1: This function is not supported in React Server Components. Please only use this export in a Client Component.

    This error happens because you’re using Provider from Redux, which is meant for client-side rendering, inside a server component. By default, files in Next.js’s app directory are treated as server components unless explicitly marked with "use client".

    Since your RootLayout needs to provide the Redux store (which must be managed client-side), you need to convert it into a client component.

    Answer: So Provide ‘use client’ in the provider

    OR: You haven’t put ‘use client’ where you are using useDispatch & useSelector

    The error you’re encountering occurs because useSelector and useDispatch hooks from Redux cannot be used in React Server Components.

    Yahoo! after the fix, it’s working now:

    Error 2: You are attemting to export ‘metadata’ from a component marked with “use client”, which is disallowed.

    Solution: Split Client and Server Logic

    We can handle this issue by splitting the Redux logic into a separate client component while keeping the RootLayout as a server component.

    Step-by-Step Fix

    1. Create a ReduxProvider Component for Client Logic

    create this Provider as component in components folder

    // components/ReduxProvider.js
    "use client"; // This component must be client-side

    import { Provider } from "react-redux";
    import { store } from "../redux/store";

    export default function ReduxProvider({ children }) {
    return <Provider store={store}>{children}</Provider>;
    }
    1. Update the Layout Without use client Directive
      This layout will remain a server component, allowing the metadata export.
    // app/layout.js
    import ReduxProvider from "../components/ReduxProvider"; // Adjust path if necessary
    import "./globals.css";

    export const metadata = {
    title: "Next.js App with Redux",
    description: "Using Redux with the App Router",
    };

    export default function RootLayout({ children }) {
    return (
    <html lang="en">
    <body>
    <ReduxProvider>{children}</ReduxProvider>
    </body>
    </html>
    );
    }

    Error 3: Cannot read properties of undefined (reading ‘getState’)

    Its already saying TypeError.

    In your store.js, you are doing Default Export

    export default store;

    But in ReduxProvider, you have done Named Import

    import { store } from "./../../../redux/store"; 
    // Named import

    Answer: Use Default Import in ReduxProvider:

    import store from "./../../../redux/store";

    Questions?

    How to not put so long like /../../ “import store from “./../../../redux/store” ?

    Solution: Using Path Aliases

    Step 1: Configure Path Aliases in jsconfig.json or tsconfig.json

    Create or modify the jsconfig.json or tsconfig.json file at the root of your project:

    jsonCopy code{
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@redux/*": ["redux/*"],
          "@components/*": ["components/*"]
        }
      }
    }
    

    In the example above:

    • @redux/* refers to the redux folder.
    • @components/* refers to the components folder.

    Step 2: Update Imports Using the Aliases

    Now, instead of using ../../../redux/store, you can import the store like this:

    javascriptCopy codeimport store from "@redux/store";
    

    For components, the path will look like this:

    javascriptCopy codeimport ReduxProvider from "@components/ReduxProvider";
    

    Step 3: Restart the Next.js Development Server

    After making changes to the configuration, restart your server:

    bashCopy codenpm run dev