Your app doesn't need Redux. Use React-Query instead.

Your app doesn't need Redux. Use React-Query instead.

The why?

I am writing this blog because when I learned how to build full-stack projects with React, I was immediately introduced to redux. Almost every full-stack JavaScript tutorial teaches you redux and you the viewer, I would assume never really questioned why this specific technology. I will say knowing redux is great and is something you should put on your resume. I am writing this because there are simpler solutions and we may just be using redux for the wrong reasons. Also when an interviewer asks you why to use Redux and what's another alternative for it, this blog post may help you answer that.

I noticed that those above-mentioned tutorials just put all their axios / fetch calls inside redux actions and store userToken globally that the entire application can use. Also, since it's a state manager, we also cache in our fetched data.

In short, we made redux a tool to call all our queries from and a place to be able to store a piece of state globally. But do we really need redux for all that?

For example, these are two axios calls embedded inside redux actions in order to like a post and another to fetch user details like how many followers they have, etc. To make it all work too, remember you need to create the redux types and reducers for them as well.

export const likePost = (upload_id) => async (dispatch, getState) => {
  try {
    dispatch({ type: LIKE_UPLOAD_REQUEST });
    const {
      userLogin: { userInfo },
    } = getState();

    const data = await fetch("/api/like-uploads/", {
      method: "POST",
      headers: { token: `${userInfo.token}`, "Content-Type": "application/json" },
      body: JSON.stringify({ upload_id }),
    });
    const parsedData = await data.json();
    dispatch({ type: LIKE_UPLOAD_SUCCESS, payload: parsedData });
  } catch (error) {
    console.log(error.message);
    dispatch({ type: LIKE_UPLOAD_FAIL, payload: error.message });
  }
};
export const getLoggedInUserDetails = () => async (dispatch, getState) => {
  try {
    dispatch({ type: LOGGED_IN_USER_DETAILS_REQUEST });

    const {
      userLogin: { userInfo },
    } = getState();

    const data = await fetch("/api/users/loggedInUserDetails", {
      method: "POST",
      headers: { token: `${userInfo.token}`, "Content-Type": "application/json" },
    });
    const parsedData = await data.json();
    dispatch({ type: LOGGED_IN_USER_DETAILS_SUCCESS, payload: parsedData });
  } catch (error) {
    console.log(error.message);
    dispatch({ type: LOGGED_IN_USER_DETAILS_FAIL, error: error.message });
  }
};

Ok so what's the problem with doing that?

The problem is we are using a machine gun to kill an ant or possibly writing a lot more code (boilerplate) to do a simple call.

Instead what we can do is utilize react-query, which allows us to store data fetched from an API into a server cache. We also have access to conditionals like isLoading and isError for a better user experience such as showing a loader while fetching the data or an error message when there is one. While we can have loading and error states within our redux actions, do you really want to write all those repetitive redux-types and reducers and maintain all that overhead code yourself? I think I'd rather save my time and let react-query handle everything.

For example to fetch userDetails this time I can call it directly in the component or page like:

 const fetchUserDeets = async () => {
    if (!userToken) return;
    const config = configWithToken(userToken);
    const { data } = await axios.get<UserInfo>("/api/users/details", config);
    return data;
  };
  const { data: userDetails, isLoading } = useQuery(`userInfo`, fetchUserDeets);

That's nice but I want to update the fetched data when I push something new such as a new task.

 const createNewTask = async () => {
    try {
      if (!userToken || !column.column_id) return;
      const config = configWithToken(userToken);
      await axios.post(
        `/api/tasks/create-task/${column.column_id}`,
        {
          title: newItemTitle,
        },
        config
      );
    } catch (error) {
      throw new Error(error.message);
    }
  };
  const {
    mutateAsync: newTask,
    isLoading,
    isError,
    error,
  } = useMutation(createNewTask, {
    onSuccess: () => queryClient.invalidateQueries(`columns-${projectId}`),
  });

With useMutation, we can create/update/delete data or perform server side-effects.

Advantages of React-Query

  1. Fetching Data becomes really easy.
  2. useMuation hook to update your fetched data.
  3. Boolean states built-in for a better user experience.
  4. You no longer have to maintain your server-side state with your own useState and useEffect calls.

How about storing something that my entire app needs to use such as a userToken?

Well to simplify things we can use a built-in hook inside of redux called useContext and wrap that context provider with the values we want around the entire app.

The useContext hook is built inside react already and can do the exact same job in terms of giving you access to userToken. You can also include some actions such as login and logout and maintain your state yourself there, but I will guarantee you it won't be as verbose compared to using redux.

With userToken global state, we are just going to be checking if there is a value or if it's null. If the user is logged in, we have access to their userToken globally and can validate access within our routes else if not, prevent a user from entering x page.

Conclussion

Redux is a great tool to learn and I think at a certain point it should. I noticed that when I was starting out, full-stack tutorials with react, almost all, used redux. I never questioned it... my app works and that's all that matters right?

Then when I was working on one of my recent projects I realized that I was writing repetitive code - reducers, types, and actions. I was primarily using redux for two things: make my api calls from and store a userToken instance that my entire application can use and occasionally update some fetched data.

Then I saw react-query and all its simplicity. In short, when dealing with data, use react-query. It'll cache your data and you can even mutate your fetched data. If you want to store a variable that your entire application can use, a built-in hook, useContext can do that for you without needing to write so much boilerplate code or installing anything.

Overall, I hope this post was helpful. Maybe redux is the right tool for you. But with the use cases mentioned above, there can be a simpler solution.