Home > Software design >  Redux vs Local State vs React Query
Redux vs Local State vs React Query

Time:11-01

I just can't decide the pattern I want to follow. I'm implementing what I call a UserParent component. Basically a list of users and when you click on a user, it loads their resources.

Approach 1: Redux

import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import { getUserResources, clearResources } from './userSlice'
import CircularProgress from '@mui/material/CircularProgress';

import { getAllUsers } from './userSlice';

export const UserParent = () => {
  const users = useSelector((state) => state.users.users  )
  const resources = useSelector((state) => state.users.user.resources  )
  const [highLightedUsers, setHighLightedItems] = useState([]);
  const isLoading = useSelector((state) => state.users.isLoading)
  let dispatch = useDispatch();

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

  const onUserClick = (user) => {
    if (highLightedUsers.includes(user.label)) {
      setHighLightedItems([])
      dispatch(clearResources())
    } else {
      setHighLightedItems([...highLightedUsers, user.label])
      dispatch(getUserResources(user.id))
    }
  }

  return(
    <>
    { isLoading === undefined || isLoading ? <CircularProgress className="search-loader" /> :
    <div className="search-container">
      <div className="search-nav">
        <NavList
          items={users}
          onItemClick={onUserClick}
          highLightedItems={highLightedUsers}
        />
      </div>
      <div className="search-results">
        <ResourceList resources={resources} />
      </div>
    </div> }
    </>
  )
}

And then we have the reducer code:

import { createSlice } from '@reduxjs/toolkit';
import Api from '../../services/api';

const INITIAL_STATE = {
  users: [],
  isLoading: true,
  user: { resources: [] }
};

export const userSlice = createSlice({
  name: 'users',
  initialState: INITIAL_STATE,
  reducers: {
    loadAllUsers: (state, action) => ({
      ...state,
      users: action.payload,
      isLoading: false
    }),
    toggleUserLoader: (state, action) => ({
      ...state,
      isLoading: action.payload
    }),
    loadUserResources: (state, action) => ({
      ...state, user: { resources: action.payload }
    }),
    clearResources: (state) => ({
      ...state,
      isLoading: false,
      user: { resources: [] }
    })
  }
});

export const {
  loadAllUsers,
  toggleUserLoader,
  loadUserResources,
  clearResources
} = userSlice.actions;

export const getAllUsers = () => async (dispatch) => {
  try {
    const res = await Api.fetchAllUsers()

    if (!res.errors) {
      dispatch(loadAllUsers(res.map(user => ({id: user.id, label: user.full_name}))));
    } else {
      throw res.errors
    }
  } catch (err) {
    alert(JSON.stringify(err))
  } 
}

export const getUserResources = (userId) => async (dispatch) => {
  try {
    const res = await Api.fetchUserResources(userId)

    if (!res.errors) {
      dispatch(loadUserResources(res));
    } else {
      throw res.errors
    }
  } catch (err) {
    alert(JSON.stringify(err))
  } 
}

export default userSlice.reducer;

This is fine but I am following this pattern on every page in my app. While it is easy follow I don't believe I'm using global state properly. Every page makes and API call and loads the response into redux, not necessarily because it needs to be shared (although it may be at some point) but because it's the pattern I'm following.

Approach 2: Local State

import React, { useEffect, useState } from 'react'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '@mui/material/CircularProgress';
import Api from '../../services/api';

export const UserParent = () => {
  const [users, setUsers] = useState([])
  const [resources, setResources] =  useState([])
  const [highLightedUsers, setHighLightedItems] = useState([]);
  const [isLoading, setIsLoading] = useState(true)

  const getUsers = async () => {
    try {
      const res = await Api.fetchAllUsers()
      setUsers(res.map(user => ({id: user.id, label: user.full_name})))
      setIsLoading(false)
    } catch (error) {
      console.log(error)
    }
  }

  const getUserResources = async (userId) => {
    try {
      setIsLoading(true)
      const res = await Api.fetchUserResources(userId)
      setResources(res)
      setIsLoading(false)
    } catch (error) {
      console.log(error)
    }
  }

  useEffect(() => {
    getUsers()
  }, [])

  const onUserClick = (user) => {
    if (highLightedUsers.includes(user.label)) {
      setHighLightedItems([])
    } else {
      setHighLightedItems([...highLightedUsers, user.label])
      getUserResources(user.id)
    }
  }

  return(
    <>
    { isLoading === undefined || isLoading ? <CircularProgress className="search-loader" /> :
    <div className="search-container">
      <div className="search-nav">
        <NavList
          items={users}
          onItemClick={onUserClick}
          highLightedItems={highLightedUsers}
        />
      </div>
      <div className="search-results">
        <ResourceList resources={resources} />
      </div>
    </div>}
    </>
  )
}

What I like about this is that it uses local state and doesn't bloat global state however, I don't like that it still has business logic in the component, I could just move these to a different file but first I wanted to try React Query instead.

Approach 3: React Query

import React, { useState } from 'react'
import { NavList } from '../nav/NavList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '@mui/material/CircularProgress';
import Api from '../../services/api';

import { useQuery } from "react-query";

export const UserParent = () => {
  const [resources, setResources] =  useState([])
  const [highLightedUsers, setHighLightedItems] = useState([]);

  const getUsers = async () => {
    try {
      const res = await Api.fetchAllUsers()
      return res
    } catch (error) {
      console.log(error)
    }
  }

  const { data, status } = useQuery("users", getUsers);

  const getUserResources = async (userId) => {
    try {
      const res = await Api.fetchUserResources(userId)
      setResources(res)
    } catch (error) {
      console.log(error)
    }
  }

  const onUserClick = (user) => {
    if (highLightedUsers.includes(user.label)) {
      setHighLightedItems([])
    } else {
      setHighLightedItems([...highLightedUsers, user.label])
      getUserResources(user.id)
    }
  }

  return(
    <>
    { status === 'loading' && <CircularProgress className="search-loader" /> }
    <div className="search-container">
      <div className="search-nav">
        <NavList
          items={data.map(user => ({id: user.id, label: user.full_name}))}
          onItemClick={onUserClick}
          highLightedItems={highLightedUsers}
        />
      </div>
      <div className="search-results">
        <ResourceList resources={resources} />
      </div>
    </div>
    </>
  )
}

This is great but there is still business logic in my component, so I can move those functions to a separate file and import them and then I end up with this:

import React, { useState } from 'react'
import { UserList } from '../users/UserList'
import { ResourceList } from '../resource/ResourceList'
import CircularProgress from '@mui/material/CircularProgress';
import { getUsers, getUserResources } from './users'
import { useQuery } from "react-query";

export const UserParent = () => {
  const [resources, setResources] =  useState([])
  const [highLightedUsers, setHighLightedItems] = useState([]);
  const { data, status } = useQuery("users", getUsers);

  const onUserClick = async (user) => {
    if (highLightedUsers.includes(user.full_name)) {
      setHighLightedItems([])
    } else {
      setHighLightedItems([...highLightedUsers, user.full_name])
      const res = await getUserResources(user.id)
      setResources(res)
    }
  }

  return(
    <>
    { status === 'loading' && <CircularProgress className="search-loader" /> }
    <div className="search-container">
      <div className="search-nav">
        <UserList
          users={data}
          onUserClick={onUserClick}
          highLightedUsers={highLightedUsers}
        />
      </div>
      <div className="search-results">
        <ResourceList resources={resources} />
      </div>
    </div>
    </>
  )
}

In my opinion this is so clean! However, is there anything wrong with the first approach using Redux? Which approach do you prefer?

CodePudding user response:

The first approach you are using shows a very outdated style of Redux.

Modern Redux is written using the official Redux Toolkit (which is the recommendation for all your production code since 2019. It does not use switch..case reducers, ACTION_TYPES, immutable reducer logic, createStore or connect. Generally, it is about 1/4 of the code.

What RTK also does is ship with RTK-Query, which is similar to React Query, but even a bit more declarative than React Query. Which one of those two you like better is probably left to personal taste.

I'd suggest that if you have any use for Redux beyond "just api fetching" (which is a solved problem given either RTK Query or React Query), you can go with Redux/RTK-Query. If you don't have any global state left after handling api caching, you should probably just go with React Query.

As for learning modern Redux including RTK Query, please follow the official Redux tutorial.

CodePudding user response:

Personally I prefer React-Query for all API-calls, it is great it useMutate and how it manages re-fetching, invalidating queries and more.

I am using your third approach where I create the queries in separate files and then import them where needed.

So far it has been great, and I am using RecoilJS for managing global states. And with the right approach there is really not much that actually needs to be in a global state IMO. Some basic auth/user info and perhaps notification management. But other than that I have been putting less and less in global states keeping it much simpler and scoped.

  • Related