Currently, I am in the process of developing the dashboard website for a music theory application company. This platform will allow users to manage various aspects such as their personal information, courses, assignments, and media content.
The dashboard site is being constructed using React hooks and Redux Toolkit. Specifically, the page I am currently focusing on is designed for teacher users to edit assignment sets, which includes assignment details and associated exercises. To ensure easy navigation, we are implementing a URL structure like
course/{courseID}/assignment/{assignmentID}
. This means that each time this page is accessed, API calls need to be made to retrieve necessary backend data.
My Redux store object is relatively straightforward. Below is an example relevant to the data on the page in question:
store: {
singleCourse: {
courseInfo: <courseInfoObj>,
students: [...<studentsObjs>],
assignments: [...<assignmentObjs>],
},
singleAssignment: {
assignmentInfo: <assignmentInfoObj>,
exercises: [...<exerciseObjs>],
},
contentLibrary: {
library: [...<libraryObjs>]
}
}
The structure of the page involves a single useEffect
utilizing batch
to group necessary dispatch
actions. Data from the Redux store is accessed using useSelector
. On this particular page, I need to execute 6 different actions (communicating with 6 API routes) to collect all required data, which is stored across the 3 reducers mentioned above. To obtain the data from Redux, I am using
const {singleCourse, singleAssignment, contentLibrary} = useSelector(state => state)
.
After observing up to 40+ re-renders on the page (especially with a higher number of exercises), I have identified that the excessive re-renders may be linked to the useSelector
calls. I am aware of memoized selectors for reducing re-renders, but integrating them into my scenario has proven challenging.
Furthermore, I am dispatching 6 actions on this page that update 3 separate reducers. Is this practice considered unfavorable?
While the pages load swiftly and are responsive, the concern of potential issues stemming from numerous re-renders persists.
Thank you.
Edit: additional code for reference.
AssignmentDetailPage.tsx
const AssignmentDetailPage = () => {
const { courseID, assignmentID } = useParams();
// dispatches
useEffect(() => {
batch(() => {
dispatch(getCourseDetail(courseID));
dispatch(getCourseStudents(courseID));
dispatch(getCourseAssignments(courseID));
dispatch(getAssignmentAndDocuments(assignmentID));
dispatch(listAssignmentDueDateExtensions(assignmentID));
dispatch(getEntireContentLibrary());
})
}, [dispatch]);
// Grabbing from Redux store.
const selectedCourse = useHarmoniaSelector(state=> state.selectedCourse);
const selectedAssignment = useHarmoniaSelector(state => state.selectedAssignment);
const contentLibrary = useHarmoniaSelector(state => state.contentLibrary);
// Retrieving assignment info from assignments state.
const assignment = selectedAssignment.assignment as Assignment || {};
const dueDateExtensions = selectedAssignment.dueDateExtensions as DueDateExtension[];
const documents = selectedAssignment.documents || [] as Document[];
// Retrieving array of assignments and determining the assignmentIndex of the current assignment.
const courseAssignments = selectedCourse.assignments as Assignment[] || [];
const courseStudents = selectedCourse.students || [] as CourseStudent[];
}
Here's a snippet related to Redux operations with the selectedAssignment:
selectedAssignment.ts
type SelectedAssignmentInitialState = {
assignment?: Assignment | null,
documents?: Document[] | null,
dueDateExtensions?: DueDateExtension[] | null
}
const initialState: SelectedAssignmentInitialState = {};
// actions
export const getAssignmentAndDocuments = createAsyncThunk
<Assignment, number, {rejectValue: void}>
('assignments/getAssignmentDetail', async(assignmentID, {dispatch, rejectWithValue}) => {
try{
const response = await API.get(`/api/assignments/${assignmentID}`);
return response.data.data as Assignment;
}catch(e){
dispatch(pushErrorNotification(errorMessage(e.data)));
return rejectWithValue();
}
});
export const listAssignmentDueDateExtensions = createAsyncThunk<DueDateExtension[], number, {rejectValue: void} >('assignment/listAssignmentDueDateExtensions', async (assignmentID, {dispatch, rejectWithValue}) => {
try{
const response = await API.get(`/api/assignments/${assignmentID}/due-day-extensions`);
return response.data.data as DueDateExtension[];
}catch(e){
dispatch(pushErrorNotification(e.data));
return rejectWithValue();
}
});
// reducer
const selectedAssignmentSlice = createSlice({
name: 'selectedAssignment',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(
getAssignmentAndDocuments.fulfilled,
((state, {payload}) => {
// assignmentDetail is everything but documents; documents kept on separate key in state obj to keep things simpler.
const {course, documents, due_at, id, released_at, set_key, show_after_due, title, weight} = payload;
state.assignment = {course, due_at, id, released_at, set_key, show_after_due, title, weight};
state.documents = documents;
})
)
.addCase(listAssignmentDueDateExtensions.fulfilled, ((state, {payload}) => {
state.dueDateExtensions = payload;
}))