0

I am building a React app with a View / ViewModel architecture.

The viewModel is responsible for fetching the data and provide data and getter functions to the View

export default function UserListViewModel() {
    const [usersList, setUsersList] = useState<User[]>([]);

    const getListUseCase = new GetUserListUseCase(new UserRepositoryImpl());

    async function getUsersList() {
        try {
            const result = await getListUseCase.invoke();
            setUsersList(result);
        } catch (err) {
            throw err;
        }
    }

    return {
        getUsersList,
        usersList,
        
    };
}
function UserListView() {
    const { usersList, getUsersList } = UserListViewModel();

    useEffect(() => {
        getUsersList();
    }, []);
 

    return (
            <>
            // View Displayed with data
            </>
        );
}

export default UserListView;

Everything is working fine, but I have an eslint warning React Hook useEffect has a missing dependency: 'getUsersList'. Either include it or remove the dependency array.eslintreact-hooks/exhaustive-deps

I cannot just follow the quick fix suggestion otherwise I would create an infinite loop pattern.

I could just disable this warning by adding 'eslint-disable-line react-hooks/exhaustive-deps' but it sound like a hack to me, and I am pretty sure there is a cleaner way to do.

Any suggestions ? Thanks.

Matos2802
  • 38
  • 9
  • Does this answer your question? [How to fix missing dependency warning when using useEffect React Hook](https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook) – devpolo Mar 22 '23 at 13:16
  • Not really because I cannot move the viewModel declaration inside the useEffect, and I am not sure if the useCallback could be used in my case. – Matos2802 Mar 22 '23 at 13:30

1 Answers1

1

By using useState in your viewmodel, you're really making it a sort of custom hook. I would argue that storing state isn't really a viewmodel concern. You also have a hidden dependency between the getUsersList and usersList where usersList starts out empty and will remain so until the consumer calls getUsersList. And by having a useState in your viewmodel, you start having restrictions on when and where you can call it: it can now only be created at the top level of your component.

Option 1

Commit to making this a custom hook and move the useEffect inside of it. You no longer really need a separate getUsersList function, so just make that your effect.

Either instantiate your GetUserListUseCase inside the effect, outside the hook, or use a useMemo so that it remains stable.

function useUsersList() {
  const [usersList, setUsersList] = useState<User[]>([]);

  useEffect(() => {
    const getListUseCase = new GetUserListUseCase(new UserRepositoryImpl());

    getListUseCase.invoke().then(setUsersList);
  }, []);

  return usersList;
}

function UserListView() {
  const usersList = useUsersList();

  return (
    <>
      usersList...
    </>
  );
}

Option 2

If you still want a separate viewmodel, you can do it but make it only responsible for constructing the UseCase and Repository and then fetching the data, and just return the results rather than setting state.

Like option 1, either instantiate your UserListViewModel inside the effect, outside the component, or use a useMemo so that it remains stable.

You could even put the useState/useEffect in a custom hook that only returns the usersList if you want to combine the two and keep the view clean.

function UserListViewModel() {
  const getListUseCase = new GetUserListUseCase(new UserRepositoryImpl());

  return {
    async getUsersList() {
      try {
        const result = await getListUseCase.invoke();
        return result;
      } catch (err) {
        throw err;
      }
    }
  };
}

function useUsersList() {
  const [usersList, setUsersList] = useState<User[]>([]);

  useEffect(() => {
    const { getUsersList } = UserListViewModel();

    getUsersList().then(setUsersList);
  }, []);

  return usersList;
}

function UserListView() {
  const usersList = useUsersList();

  return (
    <>
      usersList...
    </>
  );
}

Option 3

If you do want to keep your viewmodel design the same, you could still just use a useCallback for the getUsersList in your viewmodel and specify it as a dependency in your effect. However, then you would get another warning about a missing dependency inside that useCallback (this time for getListUseCase). You could resolve that like before, either instantiating your GetUserListUseCase inside the getUsersList, outside the viewmodel entirely, or with a useMemo:

  const getUsersList = useCallback(async function getUsersList() {
    try {
      const getListUseCase = new GetUserListUseCase(new UserRepositoryImpl());
      const result = await getListUseCase.invoke();
      setUsersList(result);
    } catch (err) {
      throw err;
    }
  }, []);
M. Desjardins
  • 605
  • 3
  • 13
  • Thank you so much for your detailed answer. I have created quite a few views based on this pattern yet, and the 3rd options looks the lightest one in terms of refacto, isn't it ? Unless you see a way to create a generic custom hook fthat I could use in whole app ? It should accept different return types (like `Event[]`, `Database[]`, ... ) and different functions constructed from different useCases inside the viewModel (for example an `updateUser` function based on `UpdateUserUseCase`) – Matos2802 Apr 03 '23 at 06:26
  • @Matos2802, while it is possible to create a generic custom hook, if you want each viewmodel to return a function with a different name based on its use case, that's going to be much more complicated. Option 1 would probably be the easiest to implement that, though, since it doesn't need a viewmodel, probably by pulling out the use case from the hook and injecting it as a parameter (adding a dependency on it to the `useEffect`). Like before you would have to instantiate it outside the component or use `useMemo`. – M. Desjardins Apr 04 '23 at 12:33