Home > Mobile >  How to prevent Redux dispatch from re-rendering the child components
How to prevent Redux dispatch from re-rendering the child components

Time:06-07

I have the below component:

import * as React from "react";
import {useAppDispatch, useAppSelector} from "./store/hook";
import {fetchUser} from "./store/slices/user/auth-slice";
import {useDispatch} from "react-redux";

function App() {
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(fetchUser());
    }, [])

    const [loading, error] = useAppSelector((state: any) => [
        state.auth.loading,
        state.auth.error,
    ])

  return (
      <>
            {loading ? (
              <div
                className="d-flex align-items-center justify-content-center"
                style={{ height: "100vh" }}
              >
                <CircularProgress color="inherit" />
              </div>
            ) : (
              <Layout>
                <Suspense fallback={<CircularProgress color="inherit" />}>
                    <Routes>
                        <Route path="/" element={<Navigate to="/regions/" replace />}/>
                        <Route path="ipam" element={<Ips />}/>
                    </Routes>
                </Suspense>
              </Layout>
            )}
            </>
  );
}

export default App;

store/slices/user/auth-slice:

import {createAsyncThunk, createSlice} from "@reduxjs/toolkit";
import UserModel from "models/userAction";
import axios from "api/axios-base";
import {getUserDetails} from "../../../api/services";
import {useAppDispatch} from "../../hook";

export const fetchUser = createAsyncThunk('auth/user', async () => {
    return getUserDetails().then((response) => {
        return response.data;
    }).catch(err => {
        return err;
    })
})


const initialState: UserModel = {
    user_id: null,
    email: '',
    name: '',
    isLoggedIn: false,
    loading: false,
    error: false,
}


const authSlice = createSlice({
    name: 'auth',
    initialState,
    reducers: {
        Login(state: any, action: any) {
            state = {
                ...initialState,
                isLoggedIn: true
            }
            return state;
        }
    },
    extraReducers: builder => {
        builder
            .addCase(fetchUser.pending, (state, action) => {
                state = {
                    ...initialState,
                    loading: true
                }
                return state;
            })
            .addCase(fetchUser.fulfilled, (state, action) => {
                state = {
                    ...initialState,
                    ...action.payload.data,
                    isLoggedIn: true,
                    loading: false
                }
                return state;
            })
            .addCase(fetchUser.rejected, (state) => {
                state = {
                    ...initialState,
                    loading: false
                }
                return state;
            });
    }
});


export const authActions = authSlice.actions;
export default authSlice;

Now the issue is Ips which is a sub-component of App, renders twice:

function Ips() {
    useEffect(() => {
        alert('test')
    }, [])


    return (
                <div className={classes.Ip}>
                    test
                </div>
    );
}

export default Ips;

hence running the alert("test") twice, I found out the issue comes after this portion of code within my authSlice turns loading True then making it false, when I comemnt it, there is no re-rendering happening to take place inside Child component.

builder
            .addCase(fetchUser.pending, (state, action) => {
                state = {
                    ...initialState,
                    loading: true
                }
                return state;
            })

How can I prevent such re-rendering?

CodePudding user response:

The problem is that you are re-rendering all app routes when calling that first fetchUser API. The child component Ips is not re-rendered, it is in fact remounted.

The simplest solution and IMO still correct (not a workaround): Initial value of loading should be true

const initialState: UserModel = {
    user_id: null,
    email: '',
    name: '',
    isLoggedIn: false,
    loading: true, // <--- `true` instead of `false`
    error: false,
}

CodePudding user response:

Your usage of useAppSelector here rerenders your application a lot more than it would need:

    const [loading, error] = useAppSelector((state: any) => [
        state.auth.loading,
        state.auth.error,
    ])

This selector creates a new array every time it is called. useAppSelector will execute your selector every time something is dispatched - and if the result is not equal (and here it is not referentially equal), it will rerender.

Call useAppSelector twice instead to keep both results stable unless they are actually changing in the store (and require a rerender):

    const loading = useAppSelector(state => state.auth.loading)
    const error= useAppSelector(state => state.auth.error)

(if you have your useAppSelector typed correctly, you don't need to specify a type for state, it will be inferred automatically)

As for why your useEffect fires twice: in React 18 with StrictMode on, during Developement every useEffect will fire twice. That's how React works.

  • Related