Selector unknown returned the root state when called. This can lead to unnecessary rerenders (warning)

1.1k Views Asked by At

When I try to go to another user's profile page I get an endless warning in console and I don't get the page and there is nothing else to do on the page so I have to close it and open new tap again.

enter image description here

profileAction.js

import { GLOBALTYPES } from "./globalTypes";
import { getDataAPI } from "../../utils/fetchData";

export const PROFILE_TYPES = {
    LOADING: 'LOADING',
    GET_USER: 'GET_USER'
}


export const getProfileUsers = ({users, id, auth}) => async (dispatch) => {
    if(users.every(user => user._id !== id)){
        
        try {
            dispatch({type: PROFILE_TYPES.LOADING, payload: true})
            const res = await getDataAPI(`/user/${id}`, auth.token)
            console.log(res)

        } catch (err) {
            dispatch({
                type: GLOBALTYPES.ALERT, 
                payload: {error: err.response.data.msg}
            })
        }
    }
}

Info.jsx

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import Avatar from '../Avatar';
import { getProfileUsers } from '../../redux/actions/profileAction';

const Info = () => {
  const { id } = useParams()
  const { auth, profile } = useSelector(state => state);
  const dispatch = useDispatch()
  
  const [userData, setUserData] = useState([])

  useEffect(() => {
    if(id === auth.user._id){
      setUserData(auth.user)
      // console.log(userData)
    }else{
      dispatch(getProfileUsers({users: profile.users, id, auth}))
    }
  }, [id, auth.user, profile.users])

  const followed = userData.followers || []

  const follow = userData.following || []
  
  return (
        <div className='info'>
        {userData ? 
          <div className="info_container" key={userData._id}>
            <Avatar src={userData.avatar} size="supper-avatar"/>
            <div className="info_content">
              <div className="info_content_title">
                <h2>{userData.username}</h2>
                <button className="btn btn-outline-info">
                  Edit Profile
                </button>
              </div>
                <div className="follow_btn">
                  <span className="mr-4">
                    {followed.length} Followers
                  </span>
                  <span className="ml-4">
                    {follow.length} Following
                  </span>
                </div>
              <h6>{userData.fullname}</h6>
              <h6>{userData.email}</h6>
            </div>
          </div> : null
      }
    </div>
  )
}

export default Info

By the way, I always get this warning before, but the pages were working fine and the warning was not endless

1

There are 1 best solutions below

2
Drew Reese On

This can happen when you are selecting "too much state", in this case you are subscribing to all state. In other words, any time any state is updated, regardless if it is related to what is destructured/used in Info, the Info component will be triggered to rerender.

The solution is to narrow the scope of what you are selecting.

const auth = useSelector(state => state.auth);
const profile = useSelector(state => state.profile);

This will trigger Info to rerender when any of the auth or profile state value changes, but not other state.

Based on the code that Info actually references, the scope can be narrowed further still.

const user = useSelector(state => state.auth.user);
const users = useSelector(state => state.profile.users);

Now this should only trigger a component rerender when specifically the user or users states update.

The render looping issues are likely related this useEffect hook call. It is a bit of a React anti-pattern to duplicate passed/selected values/"derived state" into local state. The userData value should be computed directly from the current id and user values. Just about any time you find that you have coded a useState/useEffect coupling you should really use the useMemo hook to compute (and memoize) a stable reference value.

const { id } = useParams();
const user = useSelector(state => state.auth.user);

const userData = useMemo(() => {
  return id === user._id ? user : null;
}, [id, user]);

useEffect(() => {
  if (id !== userData._id) {
    dispatch(getProfileUsers({ id }));
  }
}, [id, userData]);

This could be further improved by returning the computed user data directly in the useSelector hook.

const { id } = useParams();
const user = useSelector(state => {
  return id === user._id ? state.auth.user : null;
});

useEffect(() => {
  if (!user) {
    dispatch(getProfileUsers({ id }));
  }
}, [id, user]);

Update the getProfileUsers action to consume only an id argument and use the Thunk's getState function to access the store value. This removes the other external dependencies.

export const getProfileUsers = ({ id }) => async (dispatch, getState) => {
  const state = getState();
  const { auth, profile: { users } } = state;

  if (users.every(user => user._id !== id)) {
    try {
      dispatch({ type: PROFILE_TYPES.LOADING, payload: true });
      const res = await getDataAPI(`/user/${id}`, auth.token);
      console.log(res);

    } catch (err) {
      dispatch({
        type: GLOBALTYPES.ALERT, 
        payload: { error: err.response.data.msg }
      });
    } finally {
      dispatch({ type: PROFILE_TYPES.LOADING, payload: false });
    }
  }
}