Ehsan Nissar
Ehsan Nissar

Reputation: 683

Redux saga update a nested array of object state

I have a redux slice entity which I am using to store a state of an array. The array itself contains nested properties of array of objects. Now I need to update that state in redux saga and I am have tried to create another generator function but unable to figure out in it how can I update the state. I only need to update the state and API call will be made later. How can I update the state?

Here is the interface of task and deliveryParcels

export interface IRiderTask {
  _id?: any;
  riderId: string;
  totalAmount: number;
  riderName: string;
  cityId: string;
  cityName: string;
  status: string;
  adminName: string;
  adminId: string;
  createdAt: Date;
  deliveryParcels: IDeliveryParcels[];
}

export interface IDeliveryParcels {
  parcelId: string;
  processingStatus?: string;
  amount: number;
  orderType: 'COD' | 'NONCOD';
  postedStatus?: {
    status: string;
    statusKey: string;
    signature?: string;
    checkboxData?: any[];
    reason: string;
    adminId?: string;
    adminName?: string;
  };
}

I will be updating the postedStatus object each time with different values so need to handle it in generator function of saga.

Here is my saga

import {
  createAsyncThunk,
  createEntityAdapter,
  createSelector,
  createSlice,
  EntityState,
  PayloadAction,
} from '@reduxjs/toolkit';
import { IRiderTask } from '@swyft/interfaces';
import { put, takeEvery } from 'redux-saga/effects';
import { select } from 'typed-redux-saga/dist';
import { DeliveryManagementState } from '../state';

export const finalStatus_FEATURE_KEY = 'finalStatus';
type Processing = 'initial' | 'processing' | 'processed' | 'processing error';

/*
 * Update these interfaces according to your requirements.
 */
export interface finalStatusEntity extends IRiderTask {
  _id?: any;
}

export interface finalStatusState extends EntityState<finalStatusEntity> {
  loadingStatus: Processing;
  updatingRequest: Processing;
  error: string | null;
}

export const finalStatusAdapter = createEntityAdapter<finalStatusEntity>({
  selectId: (e) => e._id,
});

export const initialfinalStatusState: finalStatusState = finalStatusAdapter.getInitialState(
  {
    loadingStatus: 'initial',
    updatingRequest: 'initial',
    error: null,
  }
);

export const finalStatusSlice = createSlice({
  name: finalStatus_FEATURE_KEY,
  initialState: initialfinalStatusState,
  reducers: {
    add: finalStatusAdapter.addOne,
    remove: finalStatusAdapter.removeOne,
    setLoading: (state, action: PayloadAction<Processing>) => {
      state.loadingStatus = action.payload;
    },
    setTasksState: (state, action: PayloadAction<{ data: any }>) => {},
    setUpdatedState: (state, action: PayloadAction<Processing>) => {
      state.updatingRequest = action.payload;
    },
  },
});

/*
 * Export reducer for store configuration.
 */
export const finalStatusReducer = finalStatusSlice.reducer;

export const finalStatusActions = finalStatusSlice.actions;

const { selectAll, selectEntities } = finalStatusAdapter.getSelectors();

export const getfinalStatusState = (
  rootState: DeliveryManagementState
): finalStatusState => rootState.deliveryPilet.finalStatus;

export const selectAllfinalStatus = createSelector(
  getfinalStatusState,
  selectAll
);

export const selectfinalStatusEntities = createSelector(
  getfinalStatusState,
  selectEntities
);

export function* finalStatusRootSaga() {
  yield takeEvery('finalStatus/setTasksState', setTasksState);
  yield takeEvery('finalStatus/updateTaskState', updateTaskState);
}

function* setTasksState({ payload: { data } }: { payload: { data: any } }) {
  console.log('In slice', data);

  yield put(finalStatusActions.add(data));
  console.log(data);
}

function* updateTaskState(action: PayloadAction<{ id: string; data: any }>) {
  yield put(finalStatusActions.setUpdatedState('processing'));
  yield put(
    finalStatusActions.setUpdatedState({
      id: action.payload.id,
      ...action.payload.data,
    })
  );
  yield put(finalStatusActions.setUpdatedState('processed'));
}

Upvotes: 1

Views: 1290

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42170

There are two separate issues here. One is creating actions and reducer cases to properly update the data. The other is dispatching some of those actions through the saga. Quite frankly I don't really understand what your saga is supposed to be doing, and I'm not sure if you do either. But I can definitely make some improvements.

Do the "processing" states apply to the entire slice, or to each task?

Rather than updating parcels by updating elements of the array I think that it's better to treat them as a separate entity.

saga

import { EntityId, PayloadAction, Update } from "@reduxjs/toolkit";
import { finalStatusActions, FinalStatusEntity } from "./slice";
import { put, all, takeEvery, call } from "redux-saga/effects";

// dummy for API call
const postUpdates = async (
  id: EntityId,
  changes: Partial<FinalStatusEntity>
): Promise<Partial<FinalStatusEntity>> => {
  return changes;
};

function* updateTaskState(action: PayloadAction<Update<FinalStatusEntity>>) {
  const { id, changes } = action.payload;
  try {
    const result = yield call(postUpdates, id, changes);
    yield put(
      finalStatusActions.updateTaskStateSuccess({
        id,
        changes: { ...result, status: "processed" }
      })
    );
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    yield put(
      finalStatusActions.updateTaskStateFailure({ id, error: message })
    );
  }
}

export function* finalStatusRootSaga() {
  yield all([
    takeEvery(finalStatusActions.updateTaskState.type, updateTaskState)
  ]);
}

export default finalStatusRootSaga;

store

import {
  AnyAction,
  combineReducers,
  configureStore,
  getDefaultMiddleware
} from "@reduxjs/toolkit";
import {
  createSelectorHook,
  useDispatch as useBasicDispatch
} from "react-redux";
import finalStatus, { parcelSlice } from "./slice";
import createSagaMiddleware from "redux-saga";
import mySaga from "./saga";

const sagaMiddleware = createSagaMiddleware();

const deliveryPilet = combineReducers({
  finalStatus,
  parcels: parcelSlice.reducer
});

const store = configureStore({
  reducer: {
    deliveryPilet
  },
  middleware: [...getDefaultMiddleware(), sagaMiddleware]
});

sagaMiddleware.run(mySaga);

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

export type DeliveryManagementState = RootState;

export type AppDispatch = typeof store.dispatch;

export type Action = AnyAction;

export const useDispatch = (): AppDispatch => useBasicDispatch();

export const useSelector = createSelectorHook<RootState>();

export default store;

tasks

// helper for adding a task
const taskToEntity = (task: IRiderTask): FinalStatusEntity => ({
  ...task,
  parcelIds: task.deliveryParcels.map((o) => o.parcelId)
});

export const finalStatusAdapter = createEntityAdapter<FinalStatusEntity>({
  selectId: (entity) => entity._id
});

export const initialfinalStatusState = finalStatusAdapter.getInitialState();

export const finalStatusSlice = createSlice({
  name: finalStatus_FEATURE_KEY,
  initialState: initialfinalStatusState,
  reducers: {
    // accept in the form of an IRiderTask instead of FinalStatusEntity
    addTask: (state, action: PayloadAction<IRiderTask>) => {
      finalStatusAdapter.addOne(state, taskToEntity(action.payload));
    },
    removeTask: finalStatusAdapter.removeOne,
    updateTaskState: (
      state,
      action: PayloadAction<Update<FinalStatusEntity>>
    ) => {
      // here we just start the update, the saga will dispatch the rest
      const { id } = action.payload;
      finalStatusAdapter.updateOne(state, {
        id,
        changes: { status: "processing" }
      });
    },
    updateTaskStateSuccess: finalStatusAdapter.updateOne,
    updateTaskStateFailure: (
      state,
      action: PayloadAction<{ id: EntityId; error: string }>
    ) => {
      const { id, error } = action.payload;
      finalStatusAdapter.updateOne(state, {
        id,
        changes: { status: "processing error", error }
      });
    }
  }
});

export const finalStatusActions = finalStatusSlice.actions;

export default finalStatusReducer;

parcels

export const parcelAdapter = createEntityAdapter<IDeliveryParcels>({
  selectId: (parcel) => parcel.parcelId
});

export const parcelSlice = createSlice({
  name: "parcels",
  initialState: parcelAdapter.getInitialState(),
  reducers: {
    updateParcel: parcelAdapter.updateOne,
    // handle nested update
    updatePostedStatus: (
      state,
      action: PayloadAction<Update<IPostedStatus>>
    ) => {
      const { id, changes } = action.payload;
      const current = state.entities[id];
      if (current) {
        current.postedStatus = {
          ...current.postedStatus,
          ...changes
        } as IPostedStatus; // I don't know why this is necessary, but I'm getting TS errors
      }
    }
  },
  extraReducers: {
    // add the parcels when adding a task
    [finalStatusActions.addTask.type]: (
      state,
      action: PayloadAction<IRiderTask>
    ) => {
      parcelAdapter.upsertMany(state, action.payload.deliveryParcels);
    }
  }
});

types

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

export interface IRiderTask {
  _id: EntityId;
  riderId: string;
  totalAmount: number;
  riderName: string;
  cityId: string;
  cityName: string;
  status: string;
  adminName: string;
  adminId: string;
  createdAt: Date;
  deliveryParcels: IDeliveryParcels[];
}

export interface IPostedStatus {
  status: string;
  statusKey: string;
  signature?: string;
  checkboxData?: any[];
  reason: string;
  adminId?: string;
  adminName?: string;
}

export interface IDeliveryParcels {
  parcelId: string;
  processingStatus?: string;
  amount: number;
  orderType: "COD" | "NONCOD";
  postedStatus?: IPostedStatus;
}

export type Processing =
  | "initial"
  | "processing"
  | "processed"
  | "processing error";

// replace parcel objects with ids, include error
export type FinalStatusEntity = Omit<IRiderTask, "deliveryParcels"> & {
  parcelIds: string[];
  error?: string;
};

Code Sandbox Link

Upvotes: 2

Related Questions