Build Own version of Redux toolkit using Observer design pattern.
Tags: javascript, redux, react, observer
Published: 2024-04-06 13:15:42

## Introduction:

A few months ago, I built my own Redux toolkit. Now, I have completed a basic e-commerce project, and in this project, I used my own Redux toolkit.  

This project we will build `store`, `createSlice`, `createAsyncThunk`, `useDispatch`, `useSelector`from scratch.


![Preview Gif](https://github.com/rasel-mahmud-dev/rsl-redux/blob/2e0d2e1dcc86f4124a274886a31834ecda0add00/example/public/22323.gif)

## Overview

### **Key Features**

- Sync/Async actions handled.
- Store state management
- Dispatch
- RTK Query (coming soon).
- createSlice.
- createAsyncThunk.
- useDispatch.
- useSelector Hook implementation

### Live link:

[https://rsl-redux-shop.netlify.app](https://rsl-redux-shop.netlify.app/)

### Source code:

https://github.com/rasel-mahmud-dev/rsl-redux

## Project Structure

We have created a monorepo project using Lerna. It consists of two packages: a redux library and a React e-commerce project.

```markdown
Project Directory:
- .gitignore
- .idea
- README.md
- app.js
- example/
    |-- .eslintrc.cjs
    |-- .gitignore
    |-- README.md
    |-- dist
    |-- index.html
    |-- node_modules
    |-- package-lock.json
    |-- package.json
    |-- postcss.config.js
    |-- public
    |-- src/
        |-- App.css
        |-- App.jsx
        |-- assets
        |-- axios
        |-- components
        |-- index.css
        |-- layout
        |-- main.jsx
        |-- pages
        |-- routes.jsx
        |-- shared.scss
        |-- store
						│   ├── actions/
						│   ├── adminAction.js
						│   ├── authAction.js
						│   ├── cartAction.js
						│   ├── categoryAction.js
						│   ├── productAction.js
						│   ├── questionsAction.js
						│   ├── reviewAction.js
						│   └── wishlistAction.js
						│   │
						├── slices/
						│   ├── authSlice.js
						│   ├── cartSlice.js
						│   └── productSlice.js
						├── store.js
        |-- utils
    |-- tailwind.config.js
    |-- vite.config.js

- lerna.json
- node_modules
- package-lock.json
- package.json
- rsl-redux/
    - dist
    - jest.config.js
    - node_modules
    - package.json
    - readme.md
    - rollup.config.js
    - src/
        |-- configureStore.ts
        |-- createApi.ts
        |-- createAsyncAction.ts
        |-- createSlice.ts
        |-- index.ts
        |-- store.ts
        |-- useDispatch.ts
        |-- useSelector.ts
    - tsconfig.json

```

# **Now, let's create our own Redux Toolkit library code.**

Let's get started.

First of all, we will create a store which will have state, subscribe, listeners, dispatch, getState, and others.

# Create Store.js

---

```jsx
import {dispatch} from "./useDispatch"

const store = {
    state: {},
    listens: [],
    asyncActions: {},
    dispatch,
    reducerDispatch: function (reducerName, state) {
        this.state[reducerName] = {
            ...this.state[reducerName],
            ...state
        }
        this.notify()
    },
    subscribe: function (fn) {
        let index = this.listens.findIndex(lis => lis === fn)
        if (index === -1) {
            this.listens.push(fn)
        }
    },
    notify: function () {
        this.listens.forEach(lis => lis(this.state))
    },
    removeListener: function (lis) {
        this.listens = this.listens.filter(list => list !== lis)
    },
    getState(){
        return store.state
    }
}

export default store
```

Here, the state is an object, which we'll store our Redux state, subscriber, async actions and etc .

`listens` are an array where we'll store listener callback functions to be notified when an action is dispatched to update the state.

1. **state**: This property holds the current state of the redux store. It starts as an empty object `{}`.
2. **listens**: This property is an array that holds callback functions (listeners) to be notified whenever the state is updated.
3. **asyncActions**: This property is an object used to store asynchronous actions function. It used to manage asynchronous operations when dispatch some async action.
4. **dispatch**: This method is responsible for updating the state of the store. It takes a `value` parameter, which is typically an object containing the changes to be applied to the state. It merges the new value into the existing state using the spread operator (`...`) and notifies all subscribed listeners.
5. **reducerDispatch**: This method is similar to `dispatch`, but it's specifically designed for dispatching actions related to reducers. It takes two parameters: `reducerName` (the name of the reducer) and `state` (the changes to be applied to that reducer's state). It updates the state for the specified reducer and notifies listeners.
6. **subscribe**: This method allows components to subscribe to changes in the store's state. It takes a callback function (`fn`) as a parameter and adds it to the `listens` array if it's not already present.
7. **notify**: This method is called after state changes are made. It iterates over all subscribed listeners and invokes them, passing the updated state as an argument.
8. **removeListener**: This method removes a listener from the `listens` array. It takes a listener function (`lis`) as a parameter and filters it out from the array.
9. **getState**: This method returns the current state of the store.

# Create createSlice.js

---

Now we will build createSlice:

`createSlice` is a function that accepts slice name, initial state, an object of reducer functions, extra-reducers and automatically generates action creators and action types that correspond to the reducers and state.

```jsx
// createSlice.js
import store from "./store"

function createSlice(payload) {
    const {name: reducerName, reducers, extraReducers, initialState} = payload
    reducers["initialState"] = initialState
    let actions = {}

    for (let actionName in reducers) {
        let actionFn = reducers[actionName]

        actions[actionName] = function (arg) {
            return {
                actionFn,
                reducerName,
                payload: arg,
            }
        }
    }
    
    if (extraReducers) {
        extraReducers({
            addCase: function (actionCreator, reducerAction) {

                if (Object.keys((store["asyncActions"])).includes(actionCreator.type)) {
                    console.warn("Duplicate action type:: " + actionCreator.type)
                }

                if dispatch(!store["asyncActions"]) store["asyncActions"] = {}

                store["asyncActions"][actionCreator.type] = {
                    reducerName: reducerName,
                    reducerActionFn: (updatedState, result) => reducerAction(updatedState, result)
                }
            }
        })
    }

    return {
        name: reducerName,
        reducer: reducers,
        actions
    }
}

export default createSlice
```

We are already aware of the purpose of the createSlice function. It facilitates the creation of various slices, or we can say reducers, which essentially allows us to segment our redux state into smaller, manageable pieces.

### **Parameters Deconstruction:**

The function accepts a single parameter, **`payload`**, which is an object containing essential properties for defining the slice. These properties include:

- **`name`**: A string representing the name of the slice.
- **`reducers`**: An object containing synchronous action reducers that update the state.
- **`extraReducers`**: An optional function for handling  asynchronous actions.
- **`initialState`**: The initial state of the slice.

### **Reducer Initialization:**

The initial state is added to the **`reducers`** object under the key **`"initialState"`**. This ensures easy access to the initial state within the reducers.

### **Action Creation:**

For each reducer defined in the **`reducers`** object, the function generates corresponding action creator functions. These functions, when invoked, return an object encapsulating the action function, the reducer name, and the provided payload. Here's an example of how actions are created:

We store all reducer-modifying functions in the **`actions`** variable, and later, we dispatch these functions to update the slice state.

```jsx
const actions = {}
for (let actionName in reducers) {
    let actionFn = reducers[actionName]
    actions[actionName] = function (args) {
        return {
            actionFn,
            reducerName,
            payload: args,
        }
    }
}
```

When we call **`setAuth`** with the **`dispatch`** higher-order function:

```jsx
dispatch(setAuth({…}))
```

Then, in the reducers, **`setAuth`** will be called and , and it will receive the state reference and the action payload that we passed earlier when we modify it.

```jsx
 reducers: {
      setAuth(state, action) {
          state.auth = {
              ...action.payload
          }
      } 
}
```

Assigning **`state.auth`** to a certain value will update the **`auth`** property of the **`authState`**. And all subscribers will be  notified and got the latest **`authState`** updated state.

Which we will be explore later on.

### **Handling Extra Reducers:**

`use case:`

```jsx
// authSlice.js
extraReducers: (builder) => {
	  builder.addCase(loginAction.fulfilled, (state, action) => {
	      state.auth = action.payload.user
	      state.authLoaded = true
	      localStorage.setItem('token', action.payload.token)
	  })
	  
	   builder.addCase(loginAction.rejected, (state, action) => {...})
	}
```

When extra reducers are specified via the **`extraReducers`** function, they are processed to handle asynchronous actions or special cases. The function provided to **`extraReducers`** likely offers a method, such as **`addCase`**, to add new cases for handling actions. If an action type already exists, duplicate action type warnings are issued.

We invoke the **`extraReducers`** function, which takes an object as parameters. This object has an **`addCase`** method.

The **`addCase`** method takes two parameters: **`actionCreator`** and **`reducerAction`**, which will be passed when calling the **`addCase`** method with the dispatch higher-order function.

```jsx
// dispatch asynchronous action.
dispatch(fetchAllPosts())
```

Before storing, we check if the action type already exists in the `state.asyncActions` object. If it does, we don't overwrite it; instead, we throw a warning indicating that the action type should be unique.

In the `asyncActions` object, we only store two properties: `reducerName` and `reducerActionFn`. The `reducerActionFn` function takes `updatedState` and `result` as arguments. 

Additionally, the `reducerActionFn` will call the `reducerAction` function, which takes the `addCase` method as an argument.

```jsx
// createSlice.js
extraReducers({
    addCase: function (actionCreator, reducerAction) {

        if(Object.keys((store["asyncActions"])).includes(actionCreator.type)){
            console.warn("Duplicate action type:: "+ actionCreator.type)
        }

        if(!store["asyncActions"]) store["asyncActions"] = {}

        store["asyncActions"][actionCreator.type] = {
            reducerName: reducerName,
            reducerActionFn: (updatedState, result) => reducerAction(updatedState, result)
        }
        
    }
})
```

### **Return Object:**

The function returns an object containing vital information about the slice:

- **`name`**: The name of the slice.
- **`reducer`**: The original reducers object.
- **`actions`**: An object containing the action creator functions generated from the provided reducers.

# CreateAsyncThunk

---

**`createAsyncThunk`** is a utility function that used to creating asynchronous action creators. It encapsulates the common asynchronous operation patterns, such as making API requests, and handles the pending, fulfilled, and rejected states of the asynchronous operation automatically.

`use case`

```jsx
export const loginAction = createAsyncAction(
    "auth-login",
    async function (payload) {
        try {
            const {data} = await api.post("/auth/login", payload)
            return data
        } catch (e) {
            throw catchErrorMessage(e)
        }

    })
```

Here we call createAsyncAction function and pass two parameter first one **`action type`** that should be unique and second action creator function that return promise of value that received extra-reducers as action.payload or it can return promise of rejection of data. 

Now, let's look at our `createAsyncThunk`. It receives two parameters. The first one is the action type that will be stored in the store when we dispatch some action. It will retrieve the action creator function using the action type that we stored in the `store.asyncActions` object (in `createSlice.js`), and we invoke it.

Second one action payloadCreator function- 

```jsx
function createAsyncThunk(typePrefix, payloadCreator) {
    function actionCreator(arg) {
        return (dispatch, getState, extra) => {
            const result = payloadCreator(arg, {dispatch, getState, extra});
            return Object.assign(result, {
                type: typePrefix,
                arg
            })
        };
    }

    return Object.assign(actionCreator, {
        fulfilled: {type: typePrefix + "/fulfilled", payloadCreator},
        rejected: {type: typePrefix + "/rejected", payloadCreator},
        typePrefix,
    });
}

export default createAsyncThunk;
```

There, we return an object that is a copy of the actionCreator function, which will be called in the dispatch HOC function.

The actionCreator function also takes an argument that will be provided when we call the action with the dispatch higher-order function.

example:

```jsx
dispatch(loginAction({email: "", password: ""}))
```

when we call with dispatch it again return a function `return (dispatch, getState, extra) => {...}`

And 

```jsx
extraReducers: (builder) => {
		builder.addCase(loginAction.fulfilled, (state, action) => {...})
}
```

Frist when define our slice in extraReducers we define login action `fullfiled` status, will invoke  addCase method (figure 12) and pass actionCreator function that we earlier see on asyncActionThunk.js (figure 13) it return a function and has fulfilled, rejected, type key in its prototype. and as value we had set and object that has type and payloadCreator function that was provided when we create loginAction using `createAsyncAction` .

we set in store action creator async function as key action type that should be unique (figure 12). 

```jsx
// createSlice.js
extraReducers({
    addCase: function (actionCreator, reducerAction) {

        if(Object.keys((store["asyncActions"])).includes(actionCreator.type)){
            console.warn("Duplicate action type:: "+ actionCreator.type)
        }

        if(!store["asyncActions"]) store["asyncActions"] = {}

        store["asyncActions"][actionCreator.type] = {
            reducerName: reducerName,
            reducerActionFn: (updatedState, result) => reducerAction(updatedState, result)
        }
    }
})
```

```jsx
export const loginAction = createAsyncAction(
    "auth-login",
    async function (payload) {
        try {
            const {data} = await api.post("/auth/login", payload)
            return data
        } catch (e) {
            throw catchErrorMessage(e)
        }

    })
```

Don't worry if it seems confusing at first time! Once you give it a try, you'll quickly grasp it.

# Dispatch

---

Now we will see in dispatch higher order function that dispatch out action and update state. 

`useDispatch.js`

```jsx
import store from "./store";

function useDispatch() {
  return dispatch;
}

function dispatch(actionObj) {
  let actionCall;
  if (typeof actionObj === "function") {
    // handle asynchronous createAsyncThunk action.
    actionCall = actionObj(dispatch, store.getState, {});
    let action = {};

    // when action creator function return promise
    if (actionCall instanceof Promise) {
      actionCall
        .then((payloadResponse) => {
          action = {
            payload: payloadResponse,
            type: actionCall.type + "/fulfilled",
          };
        })
        .catch((ex) => {
          const type = actionCall.type;
          const lastPartActionType = type.replace(/\/[^/]*$/, "");
          action = {
            payload: ex,
            type: lastPartActionType + "/rejected",
          };
        })
        .finally(() => {
          // retrieve stored async actionCreator function from store via action type
          const reducerActionInfo = store.asyncActions[action.type];

          if (reducerActionInfo) {
            const updatedState = reducerActionInfo.reducerActionFn(
              store.state[reducerActionInfo.reducerName],
              action
            );
            store.reducerDispatch(reducerActionInfo.reducerName, updatedState);
          }
        });
    } else {
      // when action creator function return plain object, arr or other.
      action = {
        payload: actionCall,
        type: actionCall.type + "/fulfilled",
      };
      const reducerActionInfo = store.asyncActions[action.type];
      if (reducerActionInfo) {
        const updatedState = reducerActionInfo.reducerActionFn(
          store.state[reducerActionInfo.reducerName],
          action
        );
        store.reducerDispatch(reducerActionInfo.reducerName, updatedState);
      }
    }
  } else {
    // handle sync action
    const { actionFn, reducerName, payload } = actionObj;
    let sliceState = store["state"][reducerName];
    actionFn(sliceState, { payload });
    store.reducerDispatch(reducerName, sliceState);
    actionCall = payload;
  }

  return {
    unwrap() {
      return actionCall;
    },
  };
}

export default useDispatch;
```

The `useDispatch` function follows the React hook pattern, returning a function that acts as our dispatch function.

The dispatch function takes an argument, which can either be a plain object or an asynchronous function.

Here `loginAction`  is an asynchronous action. it make server post request with user email & password and return user login info. 

```jsx
dispatch(loginAction({email, password})) 
```

**If you don’t use ReactJS in your project, then you can use the `store.dispatch` method to dispatch actions.**

```jsx
store.dispatch(loginAction({email, password})) 
```

Here, loginAction represents an asynchronous action. It initiates a server POST request with the user's email and password, aiming to retrieve the user's login information. Upon successful completion, it returns the user login information.

In Figure 15, actionObj becomes a function returned by createAsyncThunk.

```jsx
// createAsyncThunk.js
return (dispatch, getState, extra) => {
    const result = payloadCreator(arg, {dispatch, getState, extra});
    return Object.assign(result, {
        type: typePrefix,
        arg
    })
}; 
```

The actionObj then returns the action result, either a promise or a plain object.

If the promise is fulfilled, we receive the result and create the action object:

```jsx
 action = {
    payload: ex,
    type: lastPartActionType + "/fulfilled",
  };
```

Otherwise, if it's rejected:

```jsx
 action = {
    payload: ex,
    type: lastPartActionType + "/rejected",
  };
```

This action is then passed to the builder.addCase method as the second parameter.

In the final block, we retrieve the async action handler function using the action type as the key, which we previously stored when calling the createSlice function.

```jsx
const reducerActionInfo = store.asyncActions[action.type];
```

We refer to it as reducerActionInfo, an object with two properties: reducerName and reducerActionFn, which are set here:

```jsx
// createSlice.js
store["asyncActions"][actionCreator.type] = {
    reducerName: reducerName,
    reducerActionFn: (updatedState, result) => reducerAction(updatedState, result)
}
```

reducerActionFn is a function that takes two arguments: the updated state used to mutate the slice state, and the action object containing the value and type. We mutate the slice state using this value:

```jsx
const updatedState = reducerActionInfo.reducerActionFn(
	  store.state[reducerActionInfo.reducerName],
	  action
);
store.reducerDispatch(reducerActionInfo.reducerName, updatedState);
```

now we call `store.reducerDispatch` method to emit event to all listener to get updated state value.

`store.js`

```jsx
  // store.js
const store = {
		...
		reducerDispatch: function (reducerName, state) {
		    this.state[reducerName] = {
		        ...this.state[reducerName],
		        ...state
		    }
		    this.notify()
		},
		subscribe: function (fn) {
		    let index = this.listens.findIndex(lis => lis === fn)
		    if (index === -1) {
		        this.listens.push(fn)
		    }
		},
		notify: function () {
		    this.listens.forEach(lis => lis(this.state))
		},
		...
}
```

The reducerDispatch method takes two arguments: the reducer name and the updated state. We immutably update our state and call the notify method, which sends all listeners the updated state.

# We are Done But wait a second;

We are done, but wait a second; we need to access the slice state and also get the updated slice state when some actions are dispatched.

To do this, we create a hook called useSelector, similar to the Redux implementation.

The useSelector hook allows us to access our reducer. It subscribes to our store behind the scenes, so when a dispatch method is called, it will be triggered,  and component  will be re-render.

# Create useSelector

---

// here full implementation..

```jsx
// useSelector.js
import { useEffect, useState } from "react";
import store from "./store";
import isArray from "./utils/isArray";
import isObject from "./utils/isObject";

function useSelector(cb) {
    let ini = {};
    const selectedState = cb(store.state);

    if(isArray(selectedState)){
        ini = [...selectedState]
    }else if(isObject(selectedState)) {
        ini = {...selectedState}
    }

    const [state, setState] = useState(ini); // Initialize state with the specific part

    const listener = (gState) => {
        const selectedState = cb(gState);
        setState(selectedState)
    };

    useEffect(() => {
        // Subscribe to the store when the components mounts
        store.subscribe(listener);

        // Clean up the subscription when the components unmounts
        return () => {
            store.removeListener(listener);
        };

    }, [selectedState]); // Only resubscribe if the callback function changes

    // Return the selected state
    return state;
}

export default useSelector;
```

The useSelector hook function takes an argument to access a specific portion of the state's value. It also subscribes to this state portion. If this portion changes, the component will re-render because it's used in the useState hook.

```jsx
 useEffect(() => {
    // Subscribe to the store when the components mounts
    store.subscribe(listener);

    // Clean up the subscription when the components unmounts
    return () => {
        store.removeListener(listener);
    };

}, [selectedState]); // Only resubscribe if the callback function changes
```

Inside useEffect, we establish the subscription to the store

```jsx
const [state, setState] = useState(ini); // Initialize state with the specific part

const listener = (gState) => {
    const selectedState = cb(gState);
    setState(selectedState)
};
```

This is our listener function.

When an action is dispatched, the listener function is invoked because we store its reference. In this function, we call the cb callback passed from the component, which returns a portion of our state value. 

We then store this returned value using the useState hook, triggering a re-render in our component.

# Create configure Store:

---

```jsx
// configureStore.js
import store from "./store";

function configureStore({reducer}) {
    for (const reducerKey in reducer) {
        const initialState = reducer[reducerKey]?.["initialState"]
        store.state = {
            ...store.state,
            [reducerKey]: initialState || undefined
        }
    }
    return store
}

export default configureStore
```

The purpose of the **`configureStore`** function in the provided code snippet is to initialize the global state of the Redux-like store with the initial state values provided for each reducer. and also it return our application `store` object reference.

# Finally we are done.

Our redux driver code completed: now we use these to our application.

### With Vanila JS Project:

```jsx
import {createSlice, configureStore} from "rsl-redux"

const blogSlice = createSlice({
    name: "blogState",
    initialState: {
        posts: []
    },
    reducers: {
        setPosts(state, action) {
            state.posts = action.payload
        },
        removePost(state, action) {
            const updatedPosts = [...state.posts]
            state.posts = updatedPosts.filter(post => post.id !== action.payload)
        }
    }
})

const {setPosts, removePost} = blogSlice.actions

const store = configureStore({
    reducer: {
        [blogSlice.name]: blogSlice.reducer
    }
})

// subscription added
store.subscribe((store) => {
    console.log("redux state", store.getState().blogState.posts)
})

const blogs = [
    {
        "id": 2,
        "title": "blog 3",
        "slug": "blog-3"
    },
    {
        "id": 3,
        "title": "blog 4",
        "slug": "blog-4"
    },
    {
        "id": 4,
        "title": "blog 5",
        "slug": "blog-5"
    },
    {
        "id": 5,
        "title": "blog 6",
        "slug": "blog-6"
    },
    {
        "id": 6,
        "title": "blog 7",
        "slug": "blog-7"
    },
    {
        "id": 7,
        "title": "blog 8",
        "slug": "blog-8"
    },
    {
        "id": 8,
        "title": "blog 9",
        "slug": "blog-9"
    },
    {
        "id": 9,
        "title": "blog 10",
        "slug": "blog-10"
    },
    {
        "id": 10,
        "title": "blog 11",
        "slug": "blog-11"
    },
    {
        "id": 11,
        "title": "blog 12",
        "slug": "blog-12"
    }
]

// store all posts in slice.
store.dispatch(setPosts(blogs))

store.dispatch(removePost(1))
store.dispatch(removePost(2))
store.dispatch(removePost(3))
store.dispatch(removePost(4))
store.dispatch(removePost(5))
store.dispatch(removePost(6))
store.dispatch(removePost(7))
store.dispatch(removePost(8))
store.dispatch(removePost(9))
store.dispatch(removePost(10))
store.dispatch(removePost(11))
```

### Output:

```jsx
[vite] connecting...
client.ts:173 [vite] connected.
main.jsx:42 redux state (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (8) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (6) [{…}, {…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (5) [{…}, {…}, {…}, {…}, {…}]
main.jsx:42 redux state (4) [{…}, {…}, {…}, {…}]
main.jsx:42 redux state (3) [{…}, {…}, {…}]
main.jsx:42 redux state (2) [{…}, {…}]
main.jsx:42 redux state [{…}]
main.jsx:42 redux state []

```

# Now we use it in our React project.

---

### Our Project `store` root file

```jsx
import {configureStore} from "rsl-redux";
import authSlice from "./slices/authSlice.js";
import productSlice from "./slices/productSlice.js";
import cartSlice from "./slices/cartSlice.js";

const store = configureStore({
    reducer: {
        [authSlice.name]: authSlice.reducer,
        [productSlice.name]: productSlice.reducer,
        [cartSlice.name]: cartSlice.reducer
    }
})

export default store
```

# Slices

---

`authSlice.js`

### Auth slice to store auth related all state

```jsx
import { createSlice } from "rsl-redux";
import { 
	authVerifyAction, 
	createAccountAction, 
	deleteAddress, 
	deleteCustomer, 
	fetchAddresses, 
	fetchAdminCustomersProducts, 
	fetchCategoryWiseOrdersSlatsAction, 
	fetchDashboardSlatsAction, 
	fetchOrdersSlatsAction, 
	fetchOrdersSlatsSummaryAction,
	loginAction 
} from "../actions/authAction.js";

import { 
	deleteReview, 
	fetchCustomerReviews, 
	updateReviewAction
} from "../actions/reviewAction.js"
;
import { 
	deleteQuestionAnswer, 
	fetchCustomerQuestionAnswers, 
	updateQuestionAnswerAction 
} from "../actions/questionsAction.js";

const initialState = {
    auth: null,
    authLoaded: false,
    dashboardSlats: {
        "sales": [],
        "carts": [],
        "users": []
    },
    orderSlats: {},
    orderCategoryWiseSlats: {},
    dashboardSlatsSummary: {
        totalIncome: 0,
        totalSpend: 0,
        totalProducts: 0,
        totalUsers: 0,
        totalCategories: 0,
        totalOrders: 0
    },
    openSidebar: "",
    addresses: [],
    customerReviews: {},
    customerQuestions: {}
}

const authSlice = createSlice({
    name: 'authState',
    initialState,
    reducers: {
        setAuth(state, action) { state.auth = { ...action.payload } },
        logOut(state) { localStorage.removeItem("token"); state.auth = null },
        setSidebar(state, action) { state.openSidebar = action.payload }
    },
    extraReducers: (builder) => {
       builder.addCase(loginAction.fulfilled, (state, action) => {...})
			builder.addCase(loginAction.rejected, (state, action) => {...})
			builder.addCase(fetchAdminCustomersProducts.fulfilled, (state, action) => {...})
			builder.addCase(deleteCustomer.fulfilled, (state, action) => {...})
			builder.addCase(fetchOrdersSlatsAction.fulfilled, (state, action) => {...})
			builder.addCase(fetchDashboardSlatsAction.fulfilled, (state, action) => {...})
			builder.addCase(fetchAddresses.fulfilled, (state, action) => {...})
			builder.addCase(deleteAddress.fulfilled, (state, action) => {...})
			builder.addCase(fetchCategoryWiseOrdersSlatsAction.fulfilled, (state, action) =>{...})
			builder.addCase(fetchOrdersSlatsSummaryAction.fulfilled, (state, action) => {...})
			builder.addCase(createAccountAction.fulfilled, (state, action) => {...})
			builder.addCase(authVerifyAction.fulfilled, (state, action) => {...})
			builder.addCase(authVerifyAction.rejected, (state) => {...})
			builder.addCase(fetchCustomerReviews.fulfilled, (state, action) => {...})
			builder.addCase(updateReviewAction.fulfilled, (state, action) => {...})
			builder.addCase(deleteReview.fulfilled, (state, action) => {...})
			builder.addCase(fetchCustomerQuestionAnswers.fulfilled, (state, action) => {...})
			builder.addCase(updateQuestionAnswerAction.fulfilled, (state, action) => {...})
			builder.addCase(deleteQuestionAnswer.fulfilled, (state, action) => {...})

    }
});

export const { setAuth, logOut, setSidebar } = authSlice.actions;
export default authSlice;

```

`productSlice.js`

Store all products related state.

```jsx
// productSlice.js

const initialState = {
    showCategories: [], // Array<{name: string, slug: string, image: string}>
    homeProducts: {},
    products: [], // Array<Product>
    brands: [], // Array<Brand>
    specsMapping: {},
    categoryBrands: {}, // {[catSlug: string]: Brand[]}
    specs: {},
    wishlist: [],
    orders: {}, // {1: {items: Array<Orders>, count: number} }
    adminProducts: [],
    categories: [],
    filter: {
        search: "",
        categoryIds: [],
        brandIds: [],
        pageNumber: 1,
        attributes: {}
    },
    reviews: {}, // key product id
    questionAnswers: {}, // key product id
}

const productSlice = createSlice({
    name: 'productState',
    initialState: initialState,
    reducers: {

        setFilter(state, action) {
            for (let payloadKey in action.payload) {
                state.filter[payloadKey] = action.payload[payloadKey]
            }
        },

        addToWishlist(state, action) {
            state.wishlist.push(action.payload)
        },

        removeFromWishlist(state, action) {
            state.wishlist = state.wishlist.filter(item => item.productId !== action?.pyload)
        }

    },

    extraReducers: (builder) => {
				builder.addCase(fetchProducts.fulfilled, (state, action) => {...})
				builder.addCase(fetchWishlists.fulfilled, (state, action) => {...})
				builder.addCase(fetchOrdersAction.fulfilled, (state, action) => {...})
				builder.addCase(fetchAdminProducts.fulfilled, (state, action) => {...})
				builder.addCase(fetchAdminDashboardProducts.fulfilled, (state, action) => {...})
				builder.addCase(fetchCategoryBrands.fulfilled, (state, action) => {...})
				builder.addCase(fetchAttributeSpec.fulfilled, (state, action) => {...})
				builder.addCase(fetchAttributeSpecMapping.fulfilled, (state, action) => {...})
				builder.addCase(fetchCategories.fulfilled, (state, action) => {...})
				builder.addCase(deleteAdminProduct.fulfilled, (state, action) => {...})
				builder.addCase(fetchBrands.fulfilled, (state, action) => {...})
				builder.addCase(deleteBrand.fulfilled, (state, action) => {...})
				builder.addCase(deleteCategory.fulfilled, (state, action) => {...})
				
				// Reviews
				builder.addCase(fetchReviews.fulfilled, (state, action) => {...})
				builder.addCase(addReviewAction.fulfilled, (state, action) => {...})
				
				// Question and answers
				builder.addCase(fetchQuestionAnswers.fulfilled, (state, action) => {...})
				builder.addCase(addQuestionAnswerAction.fulfilled, (state, action) => {...})
    }
})

export const {removeFromWishlist, addToWishlist, setFilter} = productSlice.actions
export default productSlice

```

In **`productSlice.js`**, we handle the state related to products within our e-commerce application. This slice plays a crucial role in managing product data, including details, categories, brands, reviews, questionAnswers, product attributes and wishlist.

Asynchronous actions, managed through middleware like Redux Thunk or Redux Saga, handle tasks such as fetching product data from an API. You'll find action creators like **`fetchProducts`**, **`fetchBrands`**, **`fetchCategories`**, and **`fetchReviews`**, triggering API requests to update the Redux store with the retrieved data.

## Actions:

---

```jsx
actions/
│   ├── adminAction.js
│   ├── authAction.js
│   ├── cartAction.js
│   ├── categoryAction.js
│	  ├── productAction.js
│   ├── questionsAction.js
│   ├── reviewAction.js
│   └── wishlistAction.js
```

`authAction.js`

```jsx
// authAction.js
import {createAsyncAction} from "rsl-redux";
import {api} from "../../axios";
import catchErrorMessage from "../../utils/catchErrorMessage.js";

export const loginAction = createAsyncAction("auth-login", async function (payload) {...})

export const createAccountAction = createAsyncAction("create_account", async function (payload) {...})

export const fetchAddresses = createAsyncAction("fetchAddresses", async function () {...})

export const deleteAddress = createAsyncAction("deleteAddress", async function (id) {...})

export const authVerifyAction = createAsyncAction("verify_auth_account", async function () {...})

export const fetchAdminCustomersProducts = createAsyncAction("fetchAdminCustomersProducts", async function () {...})

export const deleteCustomer = createAsyncAction("deleteCustomer", async function (id) {...})

export const fetchDashboardSlatsAction = createAsyncAction("fetchDashboardSlatsAction", async function ({year, role, taskList = []}) {...})

export const fetchOrdersSlatsAction = createAsyncAction("fetchOrdersSlatsAction", async function ({year, role}) {...})

export const fetchCartsSlatsAction = createAsyncAction("fetchCartsSlatsAction", async function ({year, role, taskList = []}) {...})

export const fetchCategoryWiseOrdersSlatsAction = createAsyncAction("fetchCategoryWiseOrdersSlatsAction", async function ({year, role, type}) {...})

export const fetchOrdersSlatsSummaryAction = createAsyncAction("fetchOrdersSlatsSummaryAction", async function ({role, taskList}) {...})

```

create our fetch posts and delete post  async actions

```jsx
// productAction.js

import {createAsyncAction} from "rsl-redux";
import {api} from "../../axios/index.js";
import catchErrorMessage from "../../utils/catchErrorMessage.js";

export const fetchProducts = createAsyncAction("fetch-products", async function (pageNumber) {...})

export const fetchOrdersAction = createAsyncAction("fetchOrdersAction", async function (pageNumber) {...})

export const searchProductAction = createAsyncAction("search-products", async function (text) {...})

export const deleteBrand = createAsyncAction("deleteBrand", async function (brandId) {...})

```

# Access Store State

---

Now we access store state and dispatch some actions.

```jsx
import React, {useEffect, useRef, useState} from 'react';
import {useParams, useSearchParams} from "react-router-dom";
import {useDispatch, useSelector} from "rsl-redux";
import Product from "../components/Product.jsx";
import {api} from "../axios/index.js";
import Breadcrumb from "../components/Breadcrumb.jsx";
import {FaAngleRight} from "react-icons/fa";
import Loader from "../components/Loader.jsx";
import {HiBars4} from "react-icons/hi2";
import {setSidebar} from "../store/slices/authSlice.js";
import {fetchAttributeSpec, fetchAttributeSpecMapping, fetchCategoryBrands} from "../store/actions/categoryAction.js";
import Popup from "../components/Popup.jsx";
import {setFilter} from "../store/slices/productSlice.js";

const SearchProduct = () => {
    const [getQuery] = useSearchParams()
    const {categoryName} = useParams()

    const filterObj = useRef({
        attributes: {}, brandIds: [],
        categoryIds: []

    })
    const {openSidebar} = useSelector(state => state.authState)

    const {categories, filter,  categoryBrands, specs} = useSelector(state => state.productState)

    const [expandAttributes, setExpandAttributes] = useState(["brand_id"])

    const [pagination] = useState({
        page: 1,
        totalPage: 10
    })

    const dispatch = useDispatch()

    let selectedCategory = categories.find(cat => cat.slug === categoryName)

    const [searchProuduct, setSearchProduct] = useState([])

    const text = getQuery.get("search")
    const [isSearching, setSearching] = useState(false)

    useEffect(() => {
        if (categoryName) {
            dispatch(fetchCategoryBrands(categoryName))
            dispatch(fetchAttributeSpec(categoryName))
            dispatch(fetchAttributeSpecMapping())
        }
    }, [categoryName]);

    useEffect(() => {
        dispatch(setFilter({
            categoryIds: categoryName ? [categoryName] : [],
            search: text,
        }))
    }, [categoryName, text])

    useEffect(() => {
        filterProduct(filter)
    }, [filter.attributes, filter.brandIds, filter.search])

    function filterProduct(filter) {...}
    
   
     return (
	       <div>...</div>
      )
}
```

Here use, dispatch some action 

 `dispatch(fetchCategoryBrands(categoryName))`
 `dispatch(fetchAttributeSpec(categoryName))`
  `dispatch(fetchAttributeSpecMapping())`

And access state using `useSelector`   hook.  

`const {categories, filter,  categoryBrands, specs} = useSelector(state => state.productState)`

 
 
### NOTE:
Please review any unintentional errors in the writing with forgiveness and a kind perspective. Also, I apologize for my English.


#
# Part 2, I will include RTK Query soon.