Lifting State through components using Props, useState(), and useEffect()

Lifting State through components using Props, useState(), and useEffect()

Introduction

  • How do you use useState() and useEffect() hooks effectively?

  • Understand the concept of lifting state

  • Manage state and pass data between several components.

Managing Internal State - useState()

The useState() hook is crucial for saving state variables within your components.

It allows you to change the state using a setter function.

Here's a simple example:

const [state, setState] = useState<number>(0);
  • State Variable: Represents the current state.

  • Setter Function: Alters the state.

This hook observes the state of our component and triggers mutations based on certain conditions.

Structuring Components

Good practice*: creating a folder for our components: "Components".*

Each component should represent a ✨ specific ✨ part of our data structure:

Single-Responsibility Principle (SOLID Principles)

For instance, if you have a user object, you can create components for different parts of this object, like User, UserAddress, and UserCompany.

User Component

Let's create a User component with props:

const User = (props: Props) => {
    return (
        <div>
          <h2>{props.username}</h2>
          <p>Email: {props.email}</p>
          <p>Phone: {props.phone}</p>
        </div>
      );
};

Now, define our data structure for the props and map them accordingly:

interface Props {
  id: string;
  email: string;
  username: string;
  phone: string;
}

Each component should have a wrapper node, like a <div> or a React Fragment.

Handling Side Effects - useEffect()

The useEffect() hook is used for performing side effects.

Example: fetching data from an API.

We now, create a folder for our "Services" containing our logic for the API call.

For example UserService.ts

Here's an example of making an API request using Axios.

import axios from 'axios';
import { UserModel } from '../models/UserModel';

export const getUserInformation = async (): Promise<UserModel[]> => {
  const response = await axios.get('https://jsonplaceholder.typicode.com/users');
  return response.data as UserModel[];
};

Now, we will use this cal in our UserList component:

export default function UserList() {
  const [usersResponse, setUsersResponse] = useState<UserModel[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [hasError, setHasError] = useState<boolean>(false);
  const [isFinished, setIsFinished] = useState<boolean>(false);

  useEffect(() => {
    setIsLoading(true);
    setHasError(false);

    if (!isFinished) {
      fetchUserData()
        .then((response: UserModel[]) => {
          setIsLoading(false);
          setUsersResponse(response);
          setIsFinished(true);
        })
        .catch((error) => {
          setHasError(true);
          setIsLoading(false);
          return error;
        });
    }
  }, [isFinished]);

  return (
    <div>
      {usersResponse.map(user => (
        <User name={user.id} id={...user} />
      ))}
    </div>
  );
}

Some notes:

  • Dependencies: The empty array [] ensures the effect runs only once after the initial render.

  • Cleanup: If the effect has dependencies, ensure you provide a cleanup function to prevent unnecessary re-renders.

Lifting State Up

When components need to share state, lift the state up to the nearest common ancestor.

Idea: The parent component manages the state and passes it down as props.

Example

Let's say UserList is the parent component:

javascriptCopy codeconst UserList = () => {
  const [users, setUsers] = useState([]);

  // Fetch users and set state...

  return (
    <div>
      {users.map(user => (
        <User key={user.id} {...user} />
      ))}
    </div>
  );
};

The User component receives props from UserList:

javascriptCopy codeconst User = ({ id, name, username }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>{username}</p>
    </div>
  );
};

And that's how we understand lifting the state up through components.

Structuring Smaller Components

Keep your components small and reusable.

For example, instead of combining UserCompany and UserAddress into one component, keep them separate:

const UserCompany = ({ company }) => {
  return <span>{company.catchPhrase}
         </span>;
};

By doing this, we adhere to good software architecture principles, making our components more Manageable and Testable.

Conclusion

Using useState() and useEffect(), along with lifting state up, allows us to manage and share state across components efficiently.

Keeping our components small, maintainable, and reusable.

This approach not only improves performance but also makes our codebase easier to understand and work with.