Paul Cowan Contract software developer

Frustrations with React Hooks

8 min read 2390

What problems do Hooks solve?

Before I detail my current frustrations with Hooks I do want to state for the record that I am, for the most part, a fan of Hooks.

I often hear that the main reason for the existence of Hooks is to replace class components. Sadly, the main heading in the official React site’s post introducing Hooks really undersells Hooks with this not-so-bold statement:

Hooks are a new addition in React 16.8. They let you use state, and other React features without writing a class.

This explanation does not give me a lot of motivation to use Hooks apart from “classes are not cool, man”! For my money, Hooks allow us to address cross-cutting concerns in a much more elegant way than the previous patterns such as mixins, higher-order components and render props.

Functionality like logging and authentication are not component-specific, and Hooks allow us to attach this type of reusable behavior to components.

What was wrong with class components?

There is something beautiful and pure about the notion of a stateless component that takes some props and returns a React element. It is a pure function and as such, side effect free.

export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
  const Tag = `h${level}` as Taggable;

  return (
    <Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
      {children}
    </Tag>
  );
};

Unfortunately, the lack of side effects makes these stateless components a bit limited, and in the end, something somewhere must manipulate state. In React, this generally meant that side effects are added to stateful class components. These class components, often called container components, execute the side effects and pass props down to these pure stateless component functions.

There are several well-documented problems with the class-based lifecycle events. One of the biggest complaints is that you often have to repeat logic in componentDidMount and componentDidUpdate.

async componentDidMount() {
  const response = await get(`/users`);
  this.setState({ users: response.data });
};

async componentDidUpdate(prevProps) {
  if (prevProps.resource !== this.props.resource) {
    const response = await get(`/users`);
    this.setState({ users: response.data });
  }
};

If you have used React for any length of time, you will have encountered this problem.

With Hooks, this side effect code can be handled in one place using the effect Hook.

const UsersContainer: React.FC = () => {
  const [ users, setUsers ] = useState([]);
  const [ showDetails, setShowDetails ] = useState(false);

 const fetchUsers = async () => {
   const response = await get('/users');
   setUsers(response.data);
 };

 useEffect( () => {
    fetchUsers(users)
  }, [ users ]
 );

 // etc.

The useEffect Hook is a considerable improvement, but this is a big step away from the pure stateless functions we previously had. Which brings me to my first frustration.

We made a custom demo for .
No really. Click here to check it out.

This is yet another JavaScript paradigm to learn

For the record, I am a 49-year-old React fanboy. The one-way data flow will always have a place in my heart after working on an ember application with the insanity of observers and computed properties.

The problem with useEffect and friends is that it exists nowhere else in the JavaScript landscape. It is unusual and has quirks, and the only way for me to become proficient and iron out these quirks is to use it in the real world and experience some pain. No tutorial using counters is going to get me into the flow. I am a freelancer and use other frameworks apart from React, and this gives me fatigue. The fact that I need to set up the eslint-plugin-react-hooks to keep me on the straight and narrow for this specific paradigm does make me feel a bit wary.

To hell and back with the dependencies array

The useEffect Hook can take an optional second argument called the dependencies array which allows you to optimize when React would execute the effect callback. React will make a comparison between each of the values via Object.is to determine whether anything has changed. If any of the elements are different than the last render cycle, then the effect will be run against the new values.

The comparison works great for primitive JavaScript types, but the problems can arise if one of the elements is an object or an array. Object.is will compare objects and arrays by reference, and there is no way to override this functionality and supply a custom comparator.

Reference checking objects by reference is a common gotcha, and I can illustrate this with the following scaled-down version of a problem I encountered:

const useFetch = (config: ApiOptions) => {
  const  [data, setData] = useState(null);

  useEffect(() => {
    const { url, skip, take } = config;
    const resource = `${url}?$skip=${skip}&take=${take}`;
    axios({ url: resource }).then(response => setData(response.data));
  }, [config]); // <-- will fetch on each render

  return data;
};

const App: React.FC = () => {
  const data = useFetch({ url: "/users", take: 10, skip: 0 });
  return <div>{data.map(d => <div>{d})}</div>;
};

On line 14, a new object is passed into useFetch on each render if we do not do something to ensure the same object is used each time. In this scenario, it would be preferable to check this object’s fields and not the object reference.

I do understand why React has not gone down the route of doing deep object compares like we may see on things like use-deep-object-compare. You can get into some serious performance problems if not careful. I do seem to revisit this problem a lot, and there are a number of fixes for this. The more dynamic your objects are the more workarounds you start adding.

There is an eslint plugin that you really should be using with the automatic –fix setup in your text editor of choice to apply eslint fixes automatically. I do worry about any new feature that requires an external plugin to check correctness.

The fact that use-deep-object-compare, use-memo-one and others exist is a testimony to this being a common enough problem or at the very least, a point of confusion.

React relies on the order in which Hooks are called

Some of the first custom Hooks to hit the shelves were several useFetch implementations that use Hooks to query a remote API. Most skirt around the issue of calling the remote API from an event handler because Hooks can only be called from the start of a functional component.

What if the data we have has pagination links and we want to re-run the effect when the user clicks a link? Below is a simple useFetch example:

const useFetch = (config: ApiOptions): [User[], boolean] => {
  const [data, setData] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { skip, take } = config;

    api({ skip, take }).then(response => {
      setData(response);
      setLoading(false);
    });
  }, [config]);

  return [data, loading];
};

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<ApiOptions>({
    take: 10,
    skip: 0
  });

  const [users, loading] = useFetch(currentPage);

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {users.map((u: User) => (
        <div>{u.name}</div>
      ))}
      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li>
            <button onClick={() => console.log('what do we do now?')}>{n + 1}</button>
          </li>
        ))}
      </ul>
    </>
  );
};

On line 23, the useFetch Hook will be called once on the first render. On lines 35 – 38, pagination buttons are rendered but how would we call the useFetch Hook from the event handlers of these buttons?

The rules of Hooks clearly state:

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function.

Hooks need to be called in the same order each time the component renders. There are several reasons why this is the case which is beautifully articulated in this post.

You definitely cannot do this:

<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
  {n + 1}
</button>

Calling the useFetch Hook from an even handler breaks the rules of Hooks because you would break the order in which the Hooks are called on each render.

Return an executable function from the Hook

I have seen two solutions (that I like) to this problem which both follow the same theme. There is react-async-hook which returns an execute function from the Hook:

import { useAsyncCallback } from 'react-async-hook';

const AppButton = ({ onClick, children }) => {
  const asyncOnClick = useAsyncCallback(onClick);
  return (
    <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
      {asyncOnClick.loading ? '...' : children}
    </button>
  );
};

const CreateTodoButton = () => (
  <AppButton
    onClick={async () => {
      await createTodoAPI('new todo text');
    }}
  >
    Create Todo
  </AppButton>
);

The call to the useAsyncCallback Hook will return an object that has the expected loading, error and result properties along with an execute function that we can call in the event handler.

react-hooks-async takes a slightly similar approach with its useAsyncTask function.

Here is a complete example with a scaled-down version of useAsyncTask below:

const createTask = (func, forceUpdateRef) => {
  const task = {
    start: async (...args) => {
      task.loading = true;
      task.result = null;
      forceUpdateRef.current(func);
      try {
        task.result = await func(...args);
      } catch (e) {
        task.error = e;
      }
      task.loading = false;
      forceUpdateRef.current(func);
    },
    loading: false,
    result: null,
    error: undefined
  };
  return task;
};

export const useAsyncTask = (func) => {
  const forceUpdate = useForceUpdate();
  const forceUpdateRef = useRef(forceUpdate);
  const task = useMemo(() => createTask(func, forceUpdateRef), [func]);

  useEffect(() => {
    forceUpdateRef.current = f => {
      if (f === func) {
        forceUpdate({});
      }
    };
    const cleanup = () => {
      forceUpdateRef.current = () => null;
    };
    return cleanup;
  }, [func, forceUpdate]);

  return useMemo(
    () => ({
      start: task.start,
      loading: task.loading,
      error: task.error,
      result: task.result
    }),
    [task.start, task.loading, task.error, task.result]
  );
};

The createTask function returns a task object with this interface:

interface Task {
  start: (...args: any[]) => Promise<void>;
  loading: boolean;
  result: null;
  error: undefined;
}

The task has the loading, error and result states that we would expect but it also returns a start function that we can call at a later date.

A task created by createTask does not trigger an update so forceUpdate and forceUpdateRef in useAsyncTask trigger the update instead.

We now have a start function that we can call from an event handler or at least from somewhere else apart from the very start of a functional component.

But now we have lost the ability to call our Hook when the functional component has first run. Thankfully react-hooks-async comes with a useAsyncRun function to facilitate this:

export const useAsyncRun = (
  asyncTask: ReturnType<typeof useAsyncTask>,
  ...args: any[]
) => {
  const { start } = asyncTask;
  useEffect(() => {
    start(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asyncTask.start, ...args]);
  useEffect(() => {
    const cleanup = () => {
      // clean up code here
    };
    return cleanup;
  });
};

The start function will be executed when any of the args arguments change.

The usage of the Hook now looks like this:

const App: React.FC = () => {
  const asyncTask = useFetch(initialPage);
  useAsyncRun(asyncTask);

  const { start, loading, result: users } = asyncTask;

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {(users || []).map((u: User) => (
        <div>{u.name}</div>
      ))}

      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li key={n}>
            <button onClick={() => start({ skip: 10 * n, take: 10 })}>
              {n + 1}
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};

The useFetch Hook is called at the start of the functional component in keeping with the laws of Hooks. The useAsyncRun function takes care of calling the API initially and the start function can be used in the onClick handler of the pagination buttons.

The useFetch Hook is now fit for purpose, but unfortunately, the complexity has risen. We have also introduced a closure which makes me slightly scared.

Monitor React hooks in production

It’s important to validate that everything works in your production React app as expected. If you’re interested in monitoring and tracking issues related to components AND seeing how users interact with specific components, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. The LogRocket React plugin allows you to search for user sessions where the user clicks a specific component in your app. With LogRocket you can understand how users interact with components, and surface any errors related to components not rendering.

In addition, LogRocket logs all actions and state from your Redux stores. LogRocket instruments your app to record requests/responses with headers + bodies. It also records the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Modernize how you debug your React apps – .

Conclusion

I think this useFetch example is an excellent example of my current frustrations with Hooks.

I do feel we are jumping through a few unexpected hoops for an elegant result, and I do understand why the call order of Hooks is essential. Unfortunately only having Hooks callable at the start of a functional component is limiting and I think we will still be scratching for ways around this. The useFetch solution is complex and Hooks also force you into working with closures, and I have many scars from unexpected things happening when working with closures.

Closures (like the ones passed to useEffect and useCallback) can capture old versions of props and state values. In particular, this happens if the “inputs” array is inadvertently missing one of the captured variables; this can be confusing.

Stale state due to code executing in a closure is one of the problems the Hooks linter sets out to cure. Stack Overflow has many questions about stale state in useEffect and friends. I seem to have spent an excessive amount of time twiddling various dependency arrays and wrapping functions in useCallback to avoid the stale state or infinite re-rendering. I understand why it is necessary, but it feels annoying, and there is no substitute for just going through a real-world problem to earn your stripes.

I mentioned at the beginning of the post that I am mostly in favor of Hooks, but they are deceptively complex. You will not find anything similar anywhere else on the JavaScript landscape. Having Hooks callable in every render of a functional component introduces issues that regular mixins do not. The need for a linter to enforce the pattern does make me wary, and the need for working with closures is always something that will introduce issues.

I would love to be proven wrong about this so please tell me how I have got Hooks wrong in the comments below.

 

Paul Cowan Contract software developer

72 Replies to “Frustrations with React Hooks”

  1. Exactly! They are admitting a design mistake and the fix is…. to create an entirely new paradigm?? Come on now.

    And the idea that code is duplicated between lifecycle methods has to be the flimsiest of excuses possible. Who are these people complaining about this? People that don’t know how to refactor code into a shared function? People that walk into doors because they forgot they had to open them first?

    Hooks seem like an odd form of bandwagoning to me. OOP is passe, and FP is the new hotness. But hooks now introduce state into functions. These are no longer the functions of FP. They are not referentially transparent pure functions. They are merely the procedures or subroutines of, say, Visual Basic, with some weird state mechanism grafted on. Side-effects everywhere!

    But you know what’s really great for state and side-effects? You know what paradigm we’ve had since the 1980s and earlier for this? OBJECTS AND CLASSES!

  2. Wow.. I didn’t realize there were others not happy with Hooks. I haven’t jumped on the Hooks bandwagon yet, hopefully i can hold off for a bit longer. I am happy to keep writing Class components as they are easy to read and understand; Readability and clarity are more important to me than some fancy new hotness; just the hooks syntax in itself is confusing the crap out of me, let alone all the gotchas that I will encounter later when I finally use hooks. Hopefully the gurus in the React team don’t find an excuse to remove Class components in React v17 !

  3. Hopefully more people do the same. I think a lot of people are scared to admit they don’t like hooks. Classes are more verbose, in a good way.

  4. Hooks sintax looks clean but there are many issues when we try to imitate lifecycles,so,i hope in next releases react team will solve better this dependency array,for now,hooks only do well job with CDM && CDU.

  5. I feel your pain as an oldtimer who has travelled widely in dev circles for over 20 years. Having spent time in Erlang and Haskell, I thought React Hooks would be the reason to get excited about React.

    However, it took me almost 30 minutes of experimentation and digging through the docs, blog posts, etc to finally understand why this didn’t trigger a re-render:
    “`
    const [flavors, setFlavorSelection] = useState(initFlavorsObj)
    setFlavorSelection( Object.assign( …flavorSelection, {[key]: (flavorSelection[key] += 1)}))
    “`
    This is the answer ( shallow comparison but I should have thought about that first! )
    “`
    setFlavorSelection( Object.assign({}, …flavorSelection, {[key]: (flavorSelection[key] += 1)}))
    “`
    I’m also starting to see folks use Hooks where better functional concepts such as curries, composition, etc are better suited. I also see custom hooks which are an awful amalgamation of noodle code. Devs who devote 80 hours to learning hooks would be better off learning something else. Let’s face it, React is really showing it’s age and Hooks do not get React closer to functional programming paradigms/frameworks like Elm or Reason

  6. After trying to use function components and hooks in a real-world project several times, I always returned to class components. Unless your function components are incredibly simple things can get very messy and complex very quickly. I do like the idea of function components and hooks, but I certainly don’t enjoy working with them in their current form.

    The React team’s reasoning for the introduction of hooks is also sketchy. Reusing code? Surely that comes as part of sensible design decisions, e.g. DRY. Not understanding how this.* works? That’s something that doesn’t just affect class instances, it affects JS as a whole; if you don’t understand it then your career as a JS/TS programmer will be going nowhere fast.

    The code base for a typical project also contains a lot more than components. Having one part of the code using one paradigm (function components), and another part of the code using a different paradigm (oop), is disconcerting at best.

    I don’t believe the React team will deprecate class components for a long time, and when they do I will be taking a cut of their source code to preserve class components, but when the team do eventually deprecate class components I imagine that will be the beginning of the end for React as we know it. Vue may become the de-facto standard at that point unless something new and magical arrives in the meantime.

    A new/separate React Component 2.0 would have been a better solution, in my opinion.

  7. Recently I was trying to use hooks in my project, ended giving up. It’s too painful to use, like writing multiple useEffect? I know it’s a ‘separate concern’ thing, but kinda verbose to me. Let alone the dependency array, messing up the lifescycle finally made me giving up, I’m sticking with class component for now.

  8. Hey Chris, I totally appreciate your frustrations because it reminds me of myself last July/August/September when I was getting up to speed with React Hooks. In contrast though, last week I had to delve into a colleague’s older class component React code and I found it painful. I suppose I’ve gotten so used to doing things “the new way” that it’s now painful to go back.

    Whenever I’m tasked with teaching someone React Hooks, I always encourage them to think about doing things “indirectly” rather than “directly”. So, for example, say we had an app where the user entered a UPC code off of a barcode and then pressed Submit to look up that product’s info.

    In my app I would wire up the `onChange` event of the Input element to a `useState` variable like this:

    “`
    const [ upc, setUpc ] = useState(null);

    setUpc(event.target.value)} />
    “`

    And then the Submit button would be wired up along with a `useState` variable like this:

    “`
    const [ isGetProductInfo, setIsGetProductInfo ] = useState(false);
    setIsGetProductInfo(true)}>Submit
    “`

    Then there would be a `useEffect` like this to fetch and save the product’s info:

    “`
    const [ productInfo, setProductInfo ] = useState(null);
    useEffect(() => {
    if (isGetProductInfo) {
    requests.get(`$(requests.API_ROOT())account_services/product_lookup/$(upc)`)
    .then(response => {
    setProductInfo(response.data);
    }
    .catch(error => { … }
    );
    setIsGetProductInfo(false);
    }
    }, [isGetProductInfo]);
    “`

    I realize this code is very basic but it illustrates a successful pattern of code to demonstrate using functional components, `useEffect`, and `useState` in React. Hope it helps!

  9. IMHO hooks are a poor implementation of functional reactive programming (FRP)… But it is a welcome first step into that direction. At least it provides local behaviors, so no need to read all component code to understand what is going on.

    Even better would be a real synchronous dataflow programming language for UIs, something like Lucid Synchrone did for DSPs. Unfortunately, earlier attempts like FlapJAX didn’t catch on, neither do new great libraries like SodiumFRP. RxJS seems to catch on, but is asynchronous, and not very suitable for UI programming due to its glitches. Maybe Svelte will do better. MobX already works for many people, but is limited compared to real FRP.

    Again IMO when you look at user interfaces as a circuit of signals, many hard programming problems become easier, and more scalable. You actually don’t need a virtual DOM either for this, Functional Reactive Programming is about taming mutable state, not avoiding it. You just connect the mutable cells of data, and let the runtime sort the dataflow graph according to ranks, and you get perfectly deterministic, testable and reason-able behavior.

    Too bad no popular frameworks takes such an approach, most likely because most of us are either imperative programmers, object oriented programmers, or functional programmers. Functional Reactive Programming is really different, it is actually dataflow programming, something that is very popular in spreadsheets, 3D software (Maya, Houdini, Substance Designer), and Digital Signal Processing. But not yet in the frontend world.

  10. I absolutely love hooks. They make state management so much easier to understand and read. I can understand frustration as it is a different way to think especially when coming from classes. But there is so much more flexibility with using hooks and they do offer performance enhancements. They open up a pandoras box of possibilities especially in the global state management realm. There is always pain when learning new things but do not get stuck in the older ways. Hooks are the future and I am excited to see them mature.

  11. Very good article…
    I have also noticed the repition of code in Lifecycle blocks of React. That was a point of concern, sometime you would have to repeat code in componentWillRecieveProps also. That was a bit furstrating.

  12. Wouldn’t it be easier just to make the useFetch depend on “skip”? Then in the click handler you could just update skip and React would handle refetching the data.

  13. Well put. A paradigm shift require a different way of thinking. The frustration and struggle coming from this article stems from trying to shoehorn hooks into the old way of thinking.

  14. I thought the same for a while, but now it’s clear to me this is fallacious. Yes, there is frustration as well but for beginners as they make the shift; After building a significant feature, I have been starting to hit these issued discussed by OP and I have scratched my head aplenty, this complexity cannot be blamed on incapability of the community at large in adaptation. A lot of people are even jumpshipping as the foresee the direction we are going, luckily for my main Product, I started out with Vue and it’s staying that way.

    These issues certainly need to be incrementally addressed within minor version(s) rather than leaving progress to radical changes like Facebook has been doing. I am skeptical at this point, perhaps they like shocking the community to keep themselves relevant, and then leave half-assed documentation to promote discussions, courses and discourse around react in general.

Leave a Reply