Redux causing 40+ re-renders on single page: would like help identifying/fixing issue

Viewed 65

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;
      }))


1 Answers

You would want to profile the application to find your real bottlenecks. It's not encouraged to write perfectly cached code for example and memoize everything, as that's premature optimization that affects developer productivity maintainability/bugs etc.

With that out of the way, it does make sense to try our best to code things performantly.

This is a good read on what to put in state, and try to leave out "derived data" like calculations. https://redux.js.org/usage/deriving-data-selectors

This is a good read on how useSelector works: https://react-redux.js.org/api/hooks#performance

I'm going to cherry pick some stuff that I think applies:

useSelector runs after every action. If the returned response differs in reference comparison to the last result, it will force the component to re-render.

Your useSelector call is returning the state object. The state object will differ by reference equality on every action, so it will always force a re-render.

Make a selector that returns each item in the state tree. Assuming you aren't creating new objects unless that part of state changes, those selectors won't cause re-renders by default.

Immer/Redux toolkit for example would preserve unchanged state tree portions. You should try to with standard redux as well, e.g. return old state unless reducer is acting on that state.

That's probably the big one. You should investigate exactly what is causing the re-renders, after the basic useSelector optimization (don't return whole state tree), and start looking for appropriate solutions.

Also, is it bad practice that on this particular page I dispatch 6 different actions that update 3 separate reducers?

I don't think it's bad. That's not a lot. One redux toolkit async action will fire 3 actions.

The pages load fairly quickly and are responsive, but I'm worried that all these re-renders are/will cause major issues.

Yeah, I think you need to keep an eye out for sure but again careful about premature optimization. I'd be trying to make sure no major mistakes are being made (like not leaning on useSelector default reference checks) but no more optimization beyond that. Leave room for code complexity for the proven bottlenecks.

Related