The nifty abstractions of React hooks

The nifty abstractions of React hooks

I've been using React hooks for a few months in my projects and I have a few tiny mental models to share about them. They could be helpful for developers who are new to React or seasoned ones who have just started to migrate to hooks from class-based components.

Why do hooks just fit in?

Whenever I code in React, I try to keep in the mind the essence of React which are the components, the render function, and the state variables used in the render function. The rest of all is just logistics around these core constructs.

And what is the core idea behind components? They return to React a representation of what to render. It's the first thing we write when we create a new component or when looking at an existing one while reading code. We almost always write the return statement first - with a skeleton div or an <> tag.

In short, we first think about the view and then think about the minimal state that is required to render that view. As the component grows in complexity, we fill it with the state. So, the state is added in a bottom-up manner.

This is precisely where hooks fit in. Hooks let us add state progressively by using plain old functions as opposed to littering the code with class-based constructs such as constructors, lifecycle methods, etc. Moreover, they do something unique by leveraging the array destructuring syntax of modern javascript.

While other languages abstract away the state behind getters and setters, hooks return the state variable and the function to modify it together so that it's clearly visible in all the places the state variable is used i.e., they take the focus away from the state logic and put the focus on the rendering mechanism. I think that's pretty neat.

useCapabilities, not callbacks

Consider for example the useEffect hook. Before hooks, you had to provide callbacks to the component's lifecycle methods i.e., every time you needed to do something when the component mounted, re-rendered and unmounted, you have to stuff it into the lifecycle methods. Imagine the state of affairs inside a component in a highly dynamic web app!

useEffect literally means use this effect that runs after/before these lifecycle methods anywhere and any number of times in your custom hooks. I could be using 10 custom hooks in a component which could be using 30 useEffect hooks inside them and I don't have to care about any of them. Moreover, the useState hooks can now be placed along with the useEffect related functionality.

If you think about it, this sort of mechanism can cover almost all the functionality provided by the DOM and the browser APIs. So, all the event listener code can be stuffed behind custom hooks and we can focus on creating rich UI components. And this is exactly why hooks have completely transformed the speed of crating component-based web apps. We now have hooks that are almost anything you can think of.

export default function DrawingCanvas() {
    // These are just a few of many state variables that are required in a canvas drawing web app
    const [auth, signIn, signOut] = useAuth();
    const [hoverable, hovered] = useHovered(element);
    const [elementX, elementY, positionX, positionY] = useMouse(ref);
    // Imagine the amount of chores and clutter required to register event listeners for just these three aspects before hooks.
    return (
        <>
        ...JSX of the canvas component
        </>
    )
}

In short, hooks relieves us from thinking in terms of events, listeners, and how to correctly handle their logic which is all auxiliary to the core application logic and allows us to use everything as a drop-in capability.

Hooks help us write direct code

Hooks allow us to write direct code. Code that is instantly understood at a high level rather than going through mental gymnastics to understand its purpose. Indirect code makes our heads bang.

For example, the useState hook says that use this function to hold an exclusive state variable any number of times in your components and anywhere in your custom hooks. They allow us to restructure our components once they get moderately complex.

Since there are custom hooks for almost every use case, for moderately complex components, we don't have to use patterns like higher-order components and render props which makes understanding data and state flow logic harder to understand. You know something is not right when rather than understanding the component logic, we waste a lot of time in understanding where the data is coming from and where it is going.

// Before react hooks
<WrapperOne>
  <WrapperTwo>
    <WrapperThree>
      <WrapperFour>
        <WrapperFive>
          <Component>
            <h1>Finally in the component!</h1>
          </Component>
        </WrapperFive>
      </WrapperFour>
    </WrapperThree>
  </WrapperTwo>
</WrapperOne>

// After react hooks
<Component>
  <h1>Finally in the component!</h1>
</Component>
// Too ambitious?? Okay...may two providers at max. An they can wrap only the root component
<ProviderOne>
  <ProviderTwo>
    <Component>
      <h1>Finally in the component!</h1>
    </Component>
  </ProviderTwo>
</ProviderOne>

Direct code is cognitively caring and efficient.

Hooks on steroids

The power of hooks really starts to shine when you see what can be done when the application state and the effects that are applied on state changes are neatly abstracted away from the component.

Take, for example, form submitting. The quality of your user's experience with forms can make or break the success of your application. Well-built form UI/UX contains a lot of state and feedback on user actions. But something like react-hook-forms makes the process of managing all that state extremely easy. With minimal involvement with the form markup, it provides us with a lot of state variables to track the state of the form. You are left only to handle the form logic based on your specific use case.

import { useForm } from "react-hook-form";

export default function App() {
  const {
    register,
    handleSubmit,
    // We get all these state variables without thinking about all they ways to manage them correctly
    formState: { errors, isDirty, isSubmitting, touchedFields, submitCount },
  } = useForm();
  const onSubmit = (data) => console.log(data);

  return (
    // See the minimal intervention in the markup
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} />
      <input {...register("password")} />
      <input type="submit" />
    </form>
  );
}

// Anyone who has implemented forms trying to achieve a great UX before hooks knows the pain

Another good example is to cache server state in custom hooks and pull the latest data on demand based on different components and application states. That way there's only a single source of truth for the application data. This is really neat. This frees us from tracking the global app state, providing components with different chunks of it, syncing it with the server state at arbitrary intervals, and the list goes on. And this is exactly what React Query does. I realized the insane power of hooks when I found out about React Query. And this is just the beginning.

// A very simple example of caching server state. useQuery will cache data fetched by fetchTodos with the key 'todos'. 

// Any component then can use the same cached data with the same key. There's no need to maintain a global state and sync that with the server state.

// The cache will be refreshed based upon the various config parameters provided by the library.

export const useTodos = () =>
  useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

function ComponentOne() {
  const { data } = useTodos()
}

function ComponentTwo() {
  // ✅ will get exactly the same data as ComponentOne
  const { data } = useTodos()
}

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ComponentOne />
      <ComponentTwo />
    </QueryClientProvider>
  )
}

In short, hooks bring back React to its original ethos.

A new era of web capabilities

The best thing I like about hooks is that it's not a library or framework. It's only a mental model combined with an opinionated syntax but I think the second-order effects are enormous. As the web becomes more powerful and complex, all the new capabilities and complexity can be neatly made available behind a use<insert capability> hook. We just need to understand the concepts around that capability, not the implementation details. Hooks let us put the focus back on want to render and what the minimum state variables needed to cover all the rendering phases.

Hope you liked this way of thinking about React hooks. It definitely helps me when I find myself getting stuck when the complexity of the web app starts to increase.

Resources

To learn and internalize the hooks abstraction, I recommend you these awesome resources to read in the order they are listed.