Search code examples
javascriptreactjsreact-hooksredux-toolkit

UseEffect hook doesn't rerender the component when I update the state


I have a component in which I am getting users from the database and then checking which users are not admin and displaying them in a list so that they can be approved or rejected. I am using redux toolkit as a state management library. As you can see in the code I am dispatching an action called getAllUsers() in the useEffect hook. The getAllUsers() actions fetches all the users from the database and returns and array which I destructure from users using useSelector hook.

If I pass users in the dependency array of useEffect I get infinite loop which is of course expected behaviour because the reference of the array changes.

In my user array which I get from getAllUsers action, I have user objects which contains multiple attributes like name, email etc. among those attributes I have an attribute called isAdmin. As you can see I am dispatching an action called approveUser, it simply just marks that isAdmin value to true.

How can I make it such that as I approve the user gets approved and hence gets removed from the component.

Approve User Component:

import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getAllUsers, approveUser } from "../../features/users/userSlice";
import { useStyles } from "../../hooks/useStyles";
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import {
TableContainer,
Table,
TableHead,
TableBody,
TableRow,
TableCell,
Paper,
CircularProgress,
Button,
Typography
} from "@material-ui/core";
import Checkbox from '@mui/material/Checkbox';

const Approval = () => {
const { users, isLoading } = useSelector((state) => state.user);
const dispatch = useDispatch();
const { loaderContainer } = useStyles()

useEffect(() => {
dispatch(getAllUsers());
console.log(users)
}, [])

return (
  <TableContainer component={Paper}>
    <Table aria-label="simple table">
      <TableHead>
        <TableRow>
          <TableCell><Typography variant="subtitle1">Name</Typography></TableCell>
          <TableCell><Typography variant="subtitle1">Email</Typography></TableCell>
          <TableCell></TableCell>
          <TableCell></TableCell>
        </TableRow>
      </TableHead>
        <TableBody>
          { users && users.data.map((user) => {
              if (!user.isAdmin) {
                return (
                  <TableRow key={user.name} sx={{ '&:last-child td, &:last-child th':{border: 0} }}>
                      <TableCell><Typography variant="subtitle1">{user.name}</Typography></TableCell>
                      <TableCell><Typography variant="subtitle1">{user.email}</Typography></TableCell>
                      <TableCell><Button endIcon={<CheckCircleIcon />} style={{ backgroundColor: '#FF7B00', color: 'white' }} onClick={() => {
                        dispatch(approveUser({ _id: user._id, isAdmin: true, isSuperUser: false }))
                      }} variant="contained">Approve</Button></TableCell>
                      <TableCell><Button endIcon={<CancelIcon />} style={{ backgroundColor: '#FF7B00', color: 'white' }} onClick={() => {
                        
                      }} variant="contained">Reject</Button></TableCell>
                  </TableRow>
                );
              }
            })}
        </TableBody>
    </Table>
  </TableContainer>
);   
};

export default Approval;

User Slice:

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

const users = 'http://localhost:5000/api/users';
const loginUrl = 'http://localhost:5000/api/login';
const signupUrl = 'http://localhost:5000/api/signup';
const approve = 'http://localhost:5000/api/approve';

export const loginUser = createAsyncThunk('user/loginUser', async 
(data) => {
const response = await axios.post(loginUrl, data);
return response;
})

export const signupUser = createAsyncThunk('user/signupUser', async 
(data) => {
const response = await axios.post(signupUrl, data);
return response;
})

export const getAllUsers = createAsyncThunk('user/getAllUsers', 
async () => {
const response = await axios.get(users);
return response;
})

export const approveUser = createAsyncThunk('user/approveUser', 
async (data) => {
// console.log(data)
const response = await axios.put(approve, data);
console.log(response)
return response;
})

const initialState = {
user: {},
users: [],
isLoggedIn: false,
isLoading: true
}

const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
    logOutUser: (state) => {
        state.isLoggedIn = false;
        state.user = {};
        state.isLoading = false;
    },
    getPassword: (state, action) => {
        const password = action.payload
        console.log(password)
    }
},
extraReducers: {
    [loginUser.pending]: (state) => {
        state.isLoading = false
    },
    [loginUser.fulfilled]: (state, action) => {
        state.isLoading = false
        state.user = action.payload.data
        state.isLoggedIn = action.payload.data.isLoggedIn
    },
    [loginUser.rejected]: (state) => {
        state.isLoading = false
    },
    [signupUser.pending]: (state) => {
        state.isLoading = false
    },
    [signupUser.fulfilled]: (state, action) => {
        state.isLoading = false
        state.user = action.payload.data
    },
    [signupUser.rejected]: (state) => {
        state.isLoading = false
    },
    [getAllUsers.pending]: (state) => {
        state.isLoading = false
    },
    [getAllUsers.fulfilled]: (state, action) => {
        state.isLoading = false
        state.users = action.payload.data
    },
    [getAllUsers.rejected]: (state) => {
        state.isLoading = false
    },
    [approveUser.pending]: (state) => {
        state.isLoading = false
    },
    [approveUser.fulfilled]: (state, action) => {
        state.isLoading = false
        // state.users = action.payload.data
    },
    [approveUser.rejected]: (state) => {
        state.isLoading = false
    },
}
})

export const { getPassword, logOutUser } = userSlice.actions
export default userSlice.reducer;

enter image description here


Solution

  • RTK's createReducer and createSlice use Immer internally to let you write simpler immutable update logic using "mutating" syntax.

    Take a look at Immer update patterns under // update by id.

    You can get the id of the approved user from action.meta.arg._id. With the id, you can find the index in state.users.

    Then you can update the isAdmin property of that user with state.users[id].isAdmin = true

        [approveUser.fulfilled]: (state, action) => {
          state.isLoading = false;
          const id = action.meta.arg._id;
          const foundId = state.users.findIndex((user) => user._id === id);
          if (foundId !== -1) state.users[foundId].isAdmin = true;
        },
    

    This is an optimistic update because we can assume to update the user in state.users without needing to dispatch getAllUsers again.

    Edit react rtk react toolkit r&m api (forked)