Learn React Hooks by Building a Paint App

React Hooks, introduced in version 16.8, have revolutionized the way we write components by allowing us to add state and lifecycle features to functional components. This shift has made it easier to share stateful logic between components, resulting in more concise and maintainable code.

In this tutorial, we‘ll dive deep into React Hooks by building a simple yet powerful paint app. You‘ll learn how to use the most essential hooks – useState, useEffect, useRef, useCallback and useMemo – and see how they can simplify your component logic. By the end, you‘ll have a solid grasp of hooks and be able to apply them in your own projects with confidence.

Why Use React Hooks?

Before we start coding, let‘s take a step back and examine why React Hooks have become so popular. According to the State of JS 2019 survey, over 80% of React developers have used hooks and more than 60% prefer them over class components.

The key benefits of using React Hooks are:

  1. Reusability: Hooks make it easy to extract stateful logic from components and reuse it across your app, without changing your component hierarchy. This promotes code reuse and keeps your components focused and maintainable.

  2. Readability: By centralizing related logic inside components, hooks make the code easier to follow and understand. You no longer have to jump between lifecycle methods to trace what‘s happening – it‘s all there in one place.

  3. Testability: Hooks make it straightforward to test components by extracting the stateful logic into separate functions that can be tested in isolation. This leads to more modular and maintainable test suites.

  4. Flexibility: Unlike patterns like render props or higher-order components, hooks don‘t introduce unnecessary nesting or indirection. They are a direct way to "hook into" React features like state and lifecycle.

With these benefits in mind, let‘s start building our paint app and see hooks in action!

Setting Up the Project

First, make sure you have a recent version of Node.js (>= 10.16) installed. Then, create a new React project using Create React App:

npx create-react-app paint-app
cd paint-app
npm start

This will set up a minimal React project and start the development server at http://localhost:3000. Open this URL in your browser and you should see the default Create React App page.

Adding State with useState

The useState hook is the most basic and essential hook in React. It allows us to add state to functional components by declaring "state variables". Here‘s a simple counter example:

import React, { useState } from ‘react‘;

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

In this example, we declare a state variable count using the useState hook. The hook returns an array with two elements:

  1. The current state value (count)
  2. A function to update the state (setCount)

We can use array destructuring to assign these elements to variables. The initial state value (in this case, 0) is passed as an argument to useState.

In the component‘s JSX, we render the current count value and a button with an onClick handler. Clicking the button increments the count by calling setCount(count + 1).

Note that unlike the this.setState method used in class components, the setCount function replaces the state instead of merging it. If you need to update multiple state variables at once, you can call useState multiple times:

const [count, setCount] = useState(0);
const [name, setName] = useState(‘‘);
const [isOnline, setIsOnline] = useState(false);

Using useEffect for Side Effects

The useEffect hook allows us to perform side effects in functional components. Side effects include things like subscribing to events, fetching data from an API, or manually changing the DOM.

Here‘s an example of using useEffect to fetch data from an API and update the component state:

import React, { useState, useEffect } from ‘react‘;

function DataFetcher({ url }) {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
    }

    fetchData();
  }, [url]);

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

In this component, we use useState to initialize a data state variable as an empty array. Then, in the useEffect hook, we define an asynchronous function fetchData that fetches JSON data from the specified URL and updates the state using setData.

The useEffect hook takes two arguments:

  1. A function that performs the side effect
  2. An optional array of dependencies

The effect function will run after every render by default. However, we can optimize performance by specifying an array of dependencies as the second argument. React will only re-run the effect if one of these dependencies has changed.

In our example, we pass [url] as the dependency array, which means the effect will only run when the url prop changes. This is useful for avoiding unnecessary network requests.

It‘s important to note that useEffect also lets you return a cleanup function. This function will run before the component unmounts or before the next render, and is useful for tasks like unsubscribing from events or cancelling network requests.

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // Clean up the subscription
    subscription.unsubscribe();
  };
}, [props.source]);

Accessing the DOM with useRef

The useRef hook allows us to create mutable references that persist across component re-renders. It‘s particularly useful for accessing DOM elements or storing values that don‘t require the component to re-render when they change.

Here‘s an example of using useRef to access an input element and focus it when a button is clicked:

import React, { useRef } from ‘react‘;

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

In this example, we create a reference called inputEl using useRef and pass it to the ref attribute of the input element. This gives us access to the actual DOM node through the current property.

When the button is clicked, the onButtonClick function is called, which accesses the input element via inputEl.current and calls its focus() method.

It‘s worth noting that useRef doesn‘t notify you when its content changes. Mutating the .current property doesn‘t cause a re-render, unlike state variables declared with useState. This makes useRef a good choice for storing values that change frequently but don‘t affect the visual output of your component.

Optimizing Performance with useCallback and useMemo

As your components grow more complex, you may notice performance issues related to unnecessary re-renders or expensive calculations. The useCallback and useMemo hooks can help optimize performance in these situations.

useCallback returns a memoized version of a callback function that only changes if one of its dependencies has changed. This is useful when passing callbacks to child components that rely on reference equality to prevent unnecessary renders.

import React, { useState, useCallback } from ‘react‘;

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <ChildComponent onIncrement={handleIncrement} />
      <div>Count: {count}</div>
    </div>
  );
}

In this example, the handleIncrement function is wrapped with useCallback and an empty dependency array []. This means that handleIncrement will always have the same reference across re-renders of ParentComponent, preventing unnecessary re-renders of ChildComponent.

Similarly, useMemo lets you memoize expensive calculations so they‘re only re-computed when necessary. It takes a function and an array of dependencies, and returns the memoized value of that function.

import React, { useMemo } from ‘react‘;

function ExpensiveComponent({ data }) {
  const expensiveResult = useMemo(() => {
    return expensiveCalculation(data);
  }, [data]);

  return <div>{expensiveResult}</div>;
}

Here, the expensiveCalculation function is only called when data changes. As long as data stays the same across re-renders, useMemo will return the cached result, avoiding the expensive computation.

It‘s important to use useCallback and useMemo judiciously, as they come with their own performance overhead. Only use them when you have a measurable performance bottleneck, and always profile your code to ensure they‘re actually helping.

Building Custom Hooks

One of the most powerful aspects of React Hooks is the ability to create your own custom hooks. Custom hooks allow you to extract component logic into reusable functions that can be shared across your application.

Building custom hooks follows three basic rules:

  1. The name of a custom hook must start with "use" (e.g., useFetch, useLocalStorage)
  2. A custom hook can call other hooks (e.g., useState, useEffect, useRef)
  3. A custom hook should return an array or object containing the relevant data and functions

Here‘s an example of a custom hook that manages the state of a form input:

import { useState } from ‘react‘;

function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}

This useInput hook takes an initial value and returns an object with the current value and an onChange handler function. We can use it in a component like this:

function LoginForm() {
  const email = useInput(‘‘);
  const password = useInput(‘‘);

  function handleSubmit(e) {
    e.preventDefault();
    console.log(email.value, password.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" {...email} />
      <input type="password" {...password} />
      <button type="submit">Log in</button>
    </form>
  );
}

By extracting the input state management into a custom hook, we can simplify our component and reuse the same logic across multiple forms.

Custom hooks are a powerful tool for creating modular, reusable code in React. They allow you to abstract complex logic and stateful behavior into self-contained, easy-to-test functions. As you build more complex applications with React, custom hooks will become an essential part of your toolkit.

Testing Components with Hooks

Testing React components that use hooks is similar to testing regular React components, with a few additional considerations. The main difference is that you need to ensure your tests are properly isolated and don‘t leak state between test cases.

Here are some tips for testing components with hooks:

  1. Use a testing library like React Testing Library or Enzyme. These libraries provide helpful utilities for rendering components, querying the DOM, and interacting with your components in tests.

  2. Mock API calls and other side effects. When testing components that fetch data or perform other side effects, it‘s important to mock these interactions to keep your tests predictable and isolated. You can use libraries like axios-mock-adapter or fetch-mock to mock API responses.

  3. Use act() when testing components with useEffect. If your component uses useEffect to perform side effects like subscribing to events or fetching data, you need to wrap the rendering and state updates in an act() call. This ensures that your component is fully rendered and updated before your assertions are run.

Here‘s an example of testing a component that fetches data using useEffect:

import React from ‘react‘;
import { render, screen, act } from ‘@testing-library/react‘;
import fetchMock from ‘fetch-mock‘;
import DataFetcher from ‘./DataFetcher‘;

describe(‘DataFetcher‘, () => {
  afterEach(() => {
    fetchMock.reset();
  });

  it(‘fetches data and displays it‘, async () => {
    const fakeData = [
      { id: 1, name: ‘Alice‘ },
      { id: 2, name: ‘Bob‘ },
    ];

    fetchMock.get(‘/api/data‘, fakeData);

    await act(async () => {
      render(<DataFetcher url="/api/data" />);
    });

    const listItems = screen.getAllByRole(‘listitem‘);
    expect(listItems).toHaveLength(2);
    expect(listItems[0]).toHaveTextContent(‘Alice‘);
    expect(listItems[1]).toHaveTextContent(‘Bob‘);
  });
});

In this test, we use fetch-mock to mock the API response, and act() to wait for the component to fetch data and update its state. We then use the screen object provided by React Testing Library to query the rendered DOM and make assertions about the displayed data.

By following these guidelines and using the right tools, testing components with hooks can be straightforward and effective. Regular testing will help you catch bugs early, refactor with confidence, and ensure your application is stable and maintainable.

Conclusion

In this tutorial, we‘ve taken a deep dive into React Hooks by building a paint app from scratch. We‘ve covered the most essential hooks – useState, useEffect, useRef, useCallback, and useMemo – and seen how they can simplify your component logic and promote code reuse.

We‘ve also explored more advanced topics like custom hooks and testing components with hooks. By extracting stateful logic into custom hooks, you can create modular, reusable code that‘s easy to test and maintain.

Remember, the key rules of hooks are:

  1. Only call hooks at the top level of your components (not inside loops, conditions, or nested functions)
  2. Only call hooks from React function components (not regular JavaScript functions or class components)

As you start using hooks in your own projects, keep these rules in mind and take advantage of the many benefits hooks offer – simpler code, better performance, and more maintainable applications.

Learn More

To learn more about React Hooks and other modern React features, check out these resources:

Happy coding!

Similar Posts