7 React Hooks Tips for Beginners

React

Introduction

React Hooks are a powerful tool for all React developers, but they are especially beneficial for beginners and intermediate developers who are looking to improve their code readability, reusability, and maintainability.

Imagine transitioning from using complex building blocks to perfectly tailored ones that stabilise and streamline your application. That’s what React Hooks can do for you.

There’s a full example on how to create a To-do list using react right here, check it out!

What are React Hooks and why are they useful?

React Hooks have made managing state and side effects in React functional components much easier. They were introduced to simplify and clean up our code, making it easier to read, reuse, and maintain.

Before Hooks, we were dealing with class components, which were a bit like a Rubik’s Cube – you could solve it, but it could take a while and be a bit frustrating, especially as your app grew. Imagine having to find the right piece of code among a thousand lines – a real hide and seek game!

Imagine you’re building a tower out of blocks. Before, it was like using big, heavy blocks that didn’t always fit well. But with React Hooks, it’s like having these special blocks that fit perfectly and make your tower steady and neat.

For example, want a counter that goes up when you click a button? Super simple with useState. And when you want something to happen whenever that counter changes, just use useEffect. Check the difference between implementing this using React Hooks and class components.

// Without React Hooks
class Counter extends React.Component {
  state = {
    count: 0,
  };

  componentDidMount() {
    document.title = `Count: ${this.state.count}`;
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      document.title = `Count: ${this.state.count}`;
    }
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increment
        </button>
      </div>
    );
  }
}

// With React Hooks
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

In short, React Hooks, especially useState and useEffect, are like a cool toolbox that can make your React development experience much more enjoyable and productive. They encourage you to write more modular and reusable code, which can lead to cleaner, more maintainable applications.

React Hooks also include useRef, useContext, and useReducer and a few others. You can check the full list here. These hooks can be used to manage state, perform side effects, and access context data in a variety of ways.

  1. Only use useState to manage state that is essential to your component

useState is a powerful React Hook that allows you to manage state in functional components. It can be used to keep track of anything that changes over time, such as the value of a form input field, the score in a game, or the list of items in a shopping cart.

Every time you use useState, you’re creating a new state variable. This can add up and make your components slow and difficult to reason about, so make sure to use it wisely.

Do:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0); // Essential state: count

  const handleIncrement = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

export default Counter;

Here we’re using useState to manage the essential state (count) needed for the counter component which is the correct way.

Managing essential state in your component helps to keep your code organised and maintainable. It also makes it easier to reason about how your component works and how it changes over time.

Don’t

import React, { useState } from 'react';

const UserProfile = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);  // Non-essential state: isLoggedIn

  const handleLogin = () => {
    setIsLoggedIn(true);
  };

  const handleLogout = () => {
    setIsLoggedIn(false);
  };

  return (
    <div>
      <p>User is logged in: {isLoggedIn ? 'Yes' : 'No'}</p>
      <button onClick={handleLogin}>Log In</button>
      <button onClick={handleLogout}>Log Out</button>
    </div>
  );
};

export default UserProfile;

On this component the isLoggedIn state is considered non-essential because it doesn’t directly pertain to the core functionality or purpose of the UserProfile component. The purpose of this component is to display user profile information, and the primary goal is not focused on managing the user’s login status.

Keeping track of non-essential state in your component can add complexity and make your code harder to read. Additionally, it may result in performance issues, particularly when handling a large amount of state.

  1. Use useState to manage state that is immutable.

Consider your state to be unchangeable. This means that you should never directly edit the state variable. Instead, anytime you wish to make a change, make a new copy of the state variable. This helps to maintain the reliability of your data and prevents unexpected changes, ensuring that your programme functions as expected.

Do:

import React, { useState } from 'react';

const ImmutableCounter = () => {
  const [count, setCount] = useState(0);  // Immutable state: count

  const handleIncrement = () => {
    // Update count immutably by using the function form of setCount
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

export default ImmutableCounter;

In this example, we’re using a special way to update a number (count) that cannot be changed directly. This helps follow the rules of React which make things more organized and reliable. Keeping things unchangeable makes it easier to understand and find mistakes in your code.

Don’t:

const MutableCounter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1);
  });

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
};

The reason why it is incorrect to update state variables directly in useEffect is because it can lead to infinite loops.

In this example, useEffect will be triggered after every render, and it will increment the count state variable. This means that the count state variable will be incremented continuously, which will lead to an infinite loop.

  1. Use useState to manage state that is normalised.

This means you should organize your state in a neat and organised way. Instead of lumping all your data together in one big pile, you break it down into smaller, well-organised pieces, like a well-sorted shelf of books. Each piece of data (like a book) has its own unique identifier, making it easy to find, update, or work with.

Do:

import React, { useState } from 'react';

const NormalizedProducts = () => {
  const [products, setProducts] = useState({
    1: { id: 1, name: 'Product A' },
    2: { id: 2, name: 'Product B' }
  });  // Normalized state: products

  return (
    <div>
      <ul>
        {Object.values(products).map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default NormalizedProducts;

In this case, we’re using useState to keep our data super organised. Each product has its own special ID, making it easy to find and work with. This way, we can quickly get to our stuff and make changes when needed. It’s like having a well-organised toolbox where each tool has its own slot – you can find the right tool fast.

Don’t:

import React, { useState } from 'react';

const UnnormalizedProducts = () => {
  const [products, setProducts] = useState([
    { id: 1, name: 'Product A' },
    { id: 2, name: 'Product B' }
  ]);  // Unnormalized state: products

  return (
    <div>
      <ul>
        {products.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UnnormalizedProducts;

Unnormalized state means that the data is not organized in the most efficient way. In this example, we’re using useState to manage a list of products, and each product is represented as an object in an array. If you have a lot of products in your list, using unnormalized state can make it difficult to find and update specific products. It can also lead to performance problems, especially if you are rendering a list of all of your products.

  1. Only use useEffect for side effects.

Stick to using useEffect for doing extra stuff in your React components, like grabbing data or setting up subscriptions. It’s not meant for updating the state or making the component show up on the screen. Think of it as the tool to handle things happening in the background while your component is doing its thing. So, if you need to fetch data from an API or listen for updates, that’s when you call in useEffect to do the job.

Do:

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

const SubscriptionManager = () => {
  useEffect(() => {
    // Simulating a subscription to an event
    const subscription = someEventEmitter.on('event', () => {
      // Handle the event
      console.log('Received event!');
    });

    // Cleanup: Unsubscribe when the component unmounts
    return () => {
      someEventEmitter.off('event', subscription);
    };
  }, []); // Empty dependency array

  return <p>Subscription Manager</p>;
};

export default SubscriptionManager;

This example use the useEffect to manage a subscription to an event. This is a typical side effect scenario where we subscribe to an event and ensure to unsubscribe when the component is unmounted.

Don’t:

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

const InvalidExample = () => {
  const [message, setMessage] = useState('Initial message');

  useEffect(() => {
    // This is incorrect: Effect is updating state and causing re-renders
    setMessage('Updated message');
  }, []); // Empty dependency array

  return <p>{message}</p>;
};

export default InvalidExample;

Here we’re using useEffect to update the component’s state directly. This is an incorrect use of useEffect, as its purpose is to handle side effects, not to trigger re-renders or update state directly. Always use useEffect for side effect-related tasks only.

  1. Specify dependencies

useEffect takes a list of dependencies as an argument and it will only run when one of the dependencies changes. This means telling React about the specific things (like variables or pieces of data) that it needs to keep an eye on. If any of those things change, React knows it should do something.

Do:

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

const DependencyExample = () => {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(1);

  useEffect(() => {
    // This effect will run whenever 'count' or 'multiplier' changes
    console.log('Effect ran! Count:', count, 'Multiplier:', multiplier);
  }, [count, multiplier]); // Specify dependencies: 'count' and 'multiplier'

  return (
    <div>
      <p>Count: {count}</p>
      <p>Multiplier: {multiplier}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setMultiplier(multiplier * 2)}>Double Multiplier</button>
    </div>
  );
};

export default DependencyExample;

We’re specifying [count, multiplier] as dependencies in useEffect. Specifying dependencies in useEffect can help to improve the performance and efficiency of your code, as it prevents the effect from running unnecessarily.

Don’t:

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

const IncorrectDependencyExample = () => {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(1);

  useEffect(() => {
    // This effect will run after every render
    console.log('Effect ran! Count:', count, 'Multiplier:', multiplier);
  }); // No dependencies specified

  return (
    <div>
      <p>Count: {count}</p>
      <p>Multiplier: {multiplier}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setMultiplier(multiplier * 2)}>Double Multiplier</button>
    </div>
  );
};

export default IncorrectDependencyExample;

In this instance, useEffect does not contain any dependencies. This could lead to odd behaviour since the effect will run after every render. Add all relevant dependencies at all times to ensure that the impact functions as intended. useEffect will run the effect after each render even if none of its dependents have changed if dependencies are left empty. In particular, if the effect is computationally costly, this may result in performance issues.

  1. Clean up after

UseEffect returns a cleanup function back. It will call this function before either the effect rerunning or the component unmounting. If the effect creates any resources, like timers or subscriptions, they should be cleaned up using the cleaning function.

Do:

import React, { useEffect } from 'react';

const SubscriptionExample = () => {
  useEffect(() => {
    const subscription = subscribeToSomething();

    // Cleanup: Unsubscribe when the component unmounts
    return () => {
      unsubscribeFromSomething(subscription);
    };
  }, []); // Empty dependency array

  return <p>Subscription Example</p>;
};

const subscribeToSomething = () => {
  // Simulate subscription setup
  console.log('Subscribed!');
  return 'subscription_token';
};

const unsubscribeFromSomething = (subscription) => {
  // Simulate subscription cleanup
  console.log('Unsubscribed!', subscription);
};

export default SubscriptionExample;

To tidy up after a subscription, we unsubscribe when the component is unmounted. This ensures that there aren’t any lingering subscriptions that might be problematic.

Don’t:

import React, { useEffect } from 'react';

const IncorrectCleanupExample = () => {
  useEffect(() => {
    const interval = setInterval(() => {
      console.log('Hello!');
    }, 1000);

    // Oops! No cleanup, this will cause a memory leak
  }, []); // Empty dependency array

  return <p>Incorrect Cleanup Example</p>;
};

export default IncorrectCleanupExample;

In this case, we forgot to clean up after creating an interval. This can result in a memory leak because the interval will continue to run even after the component is unmounted. Always remember to clear up after any resources or subscriptions to keep your application efficient and bug-free.

  1. Use hooks to create custom hooks

this means that you can make your own reusable tools for handling specific tasks in your React components. It’s like creating your custom LEGO pieces that fit perfectly into your projects, making your work easier and more organized. For example, if you often need to manage user authentication, you can build a custom hook that handles login, logout, and checking if a user is logged in. This way, you don’t have to recreate the same code in every component – you just use your custom hook, like a special tool in your toolbox, whenever you need it. It’s a neat and efficient way to make your React projects more manageable.

Do:

import React, { useState } from 'react';

// Create a custom hook to manage authentication state
const useAuth = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // Function to handle login
  const login = () => {
    setIsLoggedIn(true);
  };

  // Function to handle logout
  const logout = () => {
    setIsLoggedIn(false);
  };

  // Return the authentication state and functions to login and logout
  return {
    isLoggedIn,
    login,
    logout
  };
};

// A component that uses the custom authentication hook
const AuthComponent = () => {
  // Use the custom hook to get authentication state and functions
  const { isLoggedIn, login, logout } = useAuth();

  return (
    <div>
      <p>User is logged in: {isLoggedIn ? 'Yes' : 'No'}</p>
      <button onClick={login}>Log In</button>
      <button onClick={logout}>Log Out</button>
    </div>
  );
};

export default AuthComponent;

To manage the authentication status in this example, we have constructed a new hook called useAuth. This unique hook is used by the AuthComponent to manage the login and logout process.

Don’t

import React, { useState } from 'react';

// This component is incorrectly handling authentication without using a custom hook

const InvalidAuthComponent = () => {
  // State to track if the user is logged in or not
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // Function to handle login
  const handleLogin = () => {
    setIsLoggedIn(true); // Set isLoggedIn to true when the 'Log In' button is clicked
  };

  // Function to handle logout
  const handleLogout = () => {
    setIsLoggedIn(false); // Set isLoggedIn to false when the 'Log Out' button is clicked
  };

  return (
    <div>
      {/* Display whether the user is logged in or not */}
      <p>User is logged in: {isLoggedIn ? 'Yes' : 'No'}</p>
      <button onClick={handleLogin}>Log In</button> {/* Button to trigger login */}
      <button onClick={handleLogout}>Log Out</button> {/* Button to trigger logout */}
    </div>
  );
};

export default InvalidAuthComponent; // Export the component for use in other parts of the application

Instead of enclosing the logic in a custom hook, we are using useState directly within the component in this example. In addition to being against recommended practises, which call for encapsulating related logic within a custom hook for reusability, this can result in code duplication. Creating these special tools (custom hooks) is the way to go. It keeps your code clean, easy to understand, and you can use them in many places.

Conclusion

React Hooks, such as useState and useEffect, are powerful tools that can make your code more modular, reusable, clean, and maintainable.

Use useState to manage essential component state, ensuring immutability and normalization. Use useEffect to handle side effects, such as data fetching and subscriptions.

By following these principles, you can unlock a new level of efficiency and elegance in your React development journey.

Happy hooking!

Leave a Reply

Your email address will not be published. Required fields are marked *

0 Comments