I am working on the dashboard site for a music theory app company where users can manage their info, courses, assignments, media content etc.
The site is built using React hooks and Redux Toolkit. The current page I am working on is for a teacher user to edit an assignment set (assignment details and any exercises associated with that assignment). We want the user to be able to navigate to each assignment edit page by typing, so the url is something like course/{courseID}/assignment/{assignmentID}. This means that anytime this page loads I need to make API calls to grab any back-end data I need.
My overall Redux store object is fairly simple. Here's an example of it pertaining to what I'm grabbing on the page in question:
store: {
singleCourse: {
courseInfo: <courseInfoObj>,
students: [...<studentsObjs>],
assignments: [...<assignmentObjs>],
},
singleAssignment: {
assignmentInfo: <assignmentInfoObj>,
exercises: [...<exerciseObjs>],
},
contentLibrary: {
library: [...<libraryObjs>]
}
}
The page is structured so I have a single useEffect that uses batch to group all dispatch actions I need to make. Then, I grab what I need from the Redux store with useSelector. On this page I need to dispatch 6 different actions (to 6 different API routes) to get all the data I need, which is stored on the 3 reducers listed above. To get the data from Redux, I'm doing const {singleCourse, singleAssignment, contentLibrary} = useSelector(state => state)
I've noticed that the page is re-rendering up to 40+ times (and the more exercises there are to list in an assignment, the more re-renders there are). In my debugging I've noticed that when I remove any useSelector calls, the number of re-renders drops significantly. Note that I'm keeping track of the renders very crudely via a numeric variable defined just before my function page and incrementing/logging it before my JSX return.
Can grabbing things from the Redux store with useSelector be the cause of this insane amount of re-renders? I've done some research about using memoized selectors to prevent excessive re-rendering (and I can use createSelector from Redux Toolkit for this), but the examples are not relevant at all to my case and for the life of me I can't figure out how to use it in my case.
Also, is it bad practice that on this particular page I dispatch 6 different actions that update 3 separate reducers?
The pages load fairly quickly and are responsive, but I'm worried that all these re-renders are/will cause major issues.
Thanks
Edit: here's more 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);
// Grabbing assignment info from assignments state.
const assignment = selectedAssignment.assignment as Assignment || {};
const dueDateExtensions = selectedAssignment.dueDateExtensions as DueDateExtension[];
const documents = selectedAssignment.documents || [] as Document[];
// Grabbing array of assignments and finding assignmentIndex of current assignment.
const courseAssignments = selectedCourse.assignments as Assignment[] || [];
const courseStudents = selectedCourse.students || [] as CourseStudent[];
}
Here's an example of redux stuff 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;
}))