Haydenull

Haydenull

A front-end developer with a passion for using technology to increase personal efficiency and productivity 💡.
twitter

React Flight Rules

::: tip
Unless otherwise specified, the components mentioned in the text refer to functional components.
:::

Basic Concepts#

1. How should we view functional components, and what is the difference between them and class components?#

Functional components and class components are completely different mental models.

Functional components are pure functions; they simply accept props and return a React element.

Class components are classes that have their own state, lifecycle, and instance methods.

In practice, we should view functional components as pure functions and class components as classes. Forget about the lifecycle thinking; do not try to simulate lifecycle methods in functional components with various operations.

A pure function has no side effects, while our applications must have side effects to be meaningful. Therefore, React provides useEffect to handle side effects.

2. Why do components re-render?#

This is a feature of React; it re-renders components whenever props or state change. This ensures that the component's state remains consistent with the view.

Let's take an example from the official website:

The component is the chef, and React is the waiter.

image

  1. Triggering a render (delivering the guest’s order to the kitchen)
  2. Rendering the component (preparing the order in the kitchen)
  3. Committing to the DOM (placing the order on the table)

When props or state change, React triggers a re-render, which means the function is executed again.

In the final Commit phase, React tries to reuse DOM nodes as much as possible based on the function's execution results to improve performance.

So in React, re-rendering is not a bug but a feature. There is no need to worry about performance issues; React will optimize automatically.

It is recommended to refer to the official documentation along with this article: Why React Re-Renders

If you still have questions, it is advisable to review the reference materials a few more times:

3. What is state, why do we need it, and why does its value sometimes not match expectations?#

Components need to respond to user actions, and user actions can cause changes in the component's state. Therefore, we need a place to store the component's state, which is state.

When state changes, React re-renders the component. This is the mechanism of State.

This is also why the value of state may not match expectations, because each re-render is a function execution, and in each function execution, state has different values. All these renders have independent states that do not affect each other.

Here is an example:

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

  const onClick = () => {
    setInterval(() => {
      setCount(count + 1)
    }, 1000)
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={onClick}>Click me</button>
    </div>
  )
}

After clicking the button, the counter value will increase by 1 every second. However, we will find that the counter value remains at 1.

image

We can see that the timer only runs during the first execution of the function, and the state here is 0, so every time the timer executes, it retrieves the state as 0, which means the displayed value will always be 0 + 1 = 1.

We can understand state as a snapshot of function state; each render has a new snapshot, and these snapshots do not affect each other.

4. What is useMemo, and do I need to use it?#

useMemo is a Hook that can be used to cache the return value of a function.

So its only purpose is to improve performance because it can avoid repeated calculations.

Therefore, we must ensure that even when useMemo is removed, the behavior of the component does not change.

However, we should be clear that premature optimization is the root of all evil, so we should not use useMemo when there are no performance issues.

This statement is somewhat vague; when is the appropriate time to use it?

The answer is that in the vast majority of cases, we do not need to use useMemo.

According to the official documentation, we can use the following code to test the time taken for a calculation:

console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

// filter array: 0.15ms

When our calculation takes longer than 1ms, we can consider using useMemo to cache the calculation result.

Additionally, useMemo does not optimize the performance of the first render; it only helps us avoid repeated calculations when the component updates.

Since useMemo can optimize performance, why not use it everywhere?

There are three reasons:

  1. useMemo itself has overhead; it executes on every render to compare whether dependencies have changed, and this computational overhead may be greater than the computation we want to cache (especially considering reference types like arrays and objects).
  2. useMemo can make the behavior of components unpredictable, leading to bugs.
  3. useMemo can make the component's code harder to understand, increasing maintenance costs.

As mentioned earlier, React updates components by re-executing functions, while useMemo skips certain function executions, which can lead to unpredictable component behavior. Maintainers need to understand these skipped functions, which increases maintenance costs.

5. What is useCallback, and do I need to use it?#

useCallback and useMemo serve the same purpose; both are used to cache some computed results, but their usage scenarios are different.

useCallback is used to cache functions, while useMemo is used to cache values.

When a function or a value is passed as props to a child component, if that function or value does not change, the child component will not re-render.

So many people use useCallback to cache functions and useMemo to cache values.

const TodoList = ({ todos, onClick }) => {
  return (
    <ul>
      {/** ... */}
    </ul>
  )
}

const App = () => {
  const todos = useMemo(() => filterTodos(todos, tab), [todos, tab])
  
  const onClick = useCallback(() => {
    // ...
  }, [])
  
  return (
    <TodoList todos={todos} onClick={onClick} />
  )
}

In the example above, we can see that we used useMemo to cache todos and useCallback to cache onClick. Some people might think this optimizes performance because we avoid re-rendering the child component.

But in reality, this does not optimize performance because only when the child component is a memoized component will it avoid re-rendering.

const TodoList = React.memo(({ todos, onClick }) => {
  return (
    <ul>
      {/** ... */}
    </ul>
  )
})

6. What is useEffect, and what is it used for?#

useEffect is a Hook that can be used to handle side effects.

By default, it executes after every render of the component, but it can accept a dependency array, and it will only execute when the dependencies change.

The design goal of useEffect is not to provide lifecycle-like functionality in functional components, but to handle side effects, which means synchronizing the component's state with the external world.

Let's look at an example from the official website:

const ChatRoom = ({ roomId }) => {
  useEffect(() => {
    const connection = createConnection(roomId) // Create connection
    connection.connect()

    return () => {
      connection.disconnect() // Disconnect
    }
  }, [roomId])
}

// Default roomId is 'general'
// First operation changes 'general' to 'travel'
// Second operation changes 'travel' to 'music'

From the component's perspective, its behavior is as follows:

  1. When the component first renders, useEffect is triggered, connecting to the 'general' room.
  2. When roomId changes to 'travel', the component re-renders, triggering useEffect, disconnecting from the 'general' room and connecting to the 'travel' room.
  3. When roomId changes to 'music', the component re-renders, triggering useEffect, disconnecting from the 'travel' room and connecting to the 'music' room.
  4. When the component unmounts, useEffect is triggered, disconnecting from the 'music' room.

image

It looks perfect, but if we look at it from the perspective of useEffect, its behavior is as follows:

  1. The effect connects to the 'general' room until it disconnects.
  2. The effect connects to the 'travel' room until it disconnects.
  3. The effect connects to the 'music' room until it disconnects.

image

When we view useEffect from the perspective of the component, useEffect becomes a callback function, lifecycle that executes after the component has rendered or before it unmounts.

From the perspective of useEffect, we only care about how the application starts or stops synchronizing with the external world. Just like writing rendering code for components, we receive state and return JS. We do not consider what happens during mount, update, or unmount. We only focus on what a single render should look like.

Finally, let's look at this statement:

The question is not "when does this effect run" the question is "with which state does this effect synchronize with"

useEffect(fn) // all state

useEffect(fn, []) // no state

useEffect(fn, [these, states])

What matters is not when useEffect executes, but which states it synchronizes with.

State Management#

1. What is state management, and why is it important in React applications?#

State management refers to the process of tracking, updating, and maintaining data (state) within an application. In React applications, state management is particularly important because it directly affects the user interface and interactions of the application. When the state changes, React automatically updates the relevant components to reflect these changes.

The importance of state management in React applications is mainly reflected in the following aspects:

  1. Predictability: Good state management can make the behavior of the application more predictable, allowing developers to more easily trace and understand the sources of state changes.
  2. Maintainability: By organizing and managing state, code can be made easier to maintain, reducing the complexity of the application. This helps teams collaborate more efficiently on projects.
  3. Scalability: As applications become more complex, state management can help developers better organize code and logic, improving the scalability of the application.
  4. Performance Optimization: Effectively managing state can reduce unnecessary component re-renders, thereby improving application performance.

In React, there are various state management methods, such as using component internal state (like the useState Hook), the Context API, and third-party state management libraries (like Redux, MobX, or jotai).

2. How to use the useState Hook to manage state in functional components?#

useState is a built-in Hook provided by React that allows you to add and update state in functional components.

Inside a functional component, call the useState function and pass the initial state value as an argument. useState returns an array containing two elements: the current state value and a function to update the state. Typically, we use array destructuring to obtain these two values.

const [state, setState] = useState(initialState);

Here is a simple example:

import React, { useState } from 'react';

const Counter = () => {
  // Initialize the counter state using useState Hook
  const [count, setCount] = useState(0);

  // Define a function to increase the counter value
  const increment = () => {
    setCount(_count => _count + 1);
  };

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

export default Counter;

In this example, we created a simple counter component. We use the useState Hook to store the current value of the counter and define an increment function to update the counter. When the user clicks the "Increment" button, the counter value will increase.

useState is a very useful Hook, but there may be some common pitfalls during its use. Here are some issues to be aware of:

  1. Do not use useState in conditional statements: React relies on the consistent order of Hook calls to ensure proper association and management of state and side effects, so make sure to call Hooks in the same order on every render. Do not call Hooks inside loops, conditional statements, or nested functions.
// Incorrect example
if (condition) {
  const [state, setState] = useState(initialState);
}
  1. Asynchronous updates: The setState function is asynchronous. This means that when you call setState, the state update may not take effect immediately. If you need to calculate the new state based on the current state, use the functional update form of setState.
// Correct example
const increment = () => {
  setCount((prevCount) => prevCount + 1);
};
  1. Merging state during updates: Unlike setState in class components, useState in functional components does not automatically merge objects when updating state. If your state is an object, make sure to manually merge the state during updates.
const [state, setState] = useState({ key1: 'value1', key2: 'value2' });

// Incorrect example
setState({ key1: 'new-value1' }); // This will cause key2 to be lost

// Correct example
setState((prevState) => ({ ...prevState, key1: 'new-value1' }));
  1. Avoid repeated calculations during initialization: If your initial state needs to be obtained through complex calculations or side effect functions, you can pass an initial state calculation function to useState to avoid performing the calculation on every render.
const [state, setState] = useState(() => computeExpensiveInitialState());
  1. Initial value is only used during the first render: Subsequent re-renders will retain and use the already set state value, rather than reapplying initialState.

Therefore, when using useState, it is important to understand this behavior correctly. If you need to set the initial value of the state based on props or other external variables, make sure to handle these dependencies correctly in the state update logic.

The first approach is to use the key property to trigger a re-render of the component, simply set the key property to a unique value when using the component. When you need to re-render the component based on a prop (like initialCount), you can set the key to that prop value:

import React from 'react';
import MyComponent from './MyComponent';

function ParentComponent() {
  const [initialCount, setInitialCount] = useState(0)
  return <MyComponent key={initialCount} initialCount={initialCount} />;
}

export default ParentComponent;

In this example, when the initialCount prop changes, the MyComponent component will re-render using the new key value. This will cause the component to initialize and mount based on the new initialCount value.

The second approach is to use the useEffect Hook to handle changes in external variables, thereby updating the component state as needed.

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

function MyComponent({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  useEffect(() => {
    // Update component state when initialCount prop value changes
    setCount(initialCount);
  }, [initialCount]);
}

export default MyComponent;

The advantage of the key approach is that it has a lower cognitive load, and the component's state is clearer and more predictable. The downside is that changing the key will cause the entire component to unmount and mount again, which may incur a higher performance cost.

The advantage of the useEffect approach is that it only triggers a re-render when the prop value changes, without needing to unmount and mount the entire component. The performance overhead is lower. The downside is that it requires manual management of potential side effects for cleanup and reapplication, requiring more code to handle prop value changes and state updates, making the component's state more complex.

Depending on specific needs and performance requirements, a trade-off can be made between these two approaches. Personally, I believe that performance should not be considered prematurely; rather, the maintainability of the code and the predictability of state may have a greater impact on project quality. Therefore, in most scenarios, I would prioritize recommending the key approach.

3. What is the Context API, and how does it solve the problem of state sharing?#

The Context API is a method in React for sharing state throughout the component tree without explicitly passing props down through every level. It allows you to set a value at a certain level of the component tree and then access that value directly in any component lower in the tree. This is very useful for managing shared state that spans multiple levels, avoiding the hassle of passing props down through many layers.

To use the Context API, you need to follow these steps:

  1. Create a context object: Use the React.createContext function to create a new context object. This function accepts a default value as a parameter, which will be used when no matching context provider is found.
const MyContext = React.createContext(defaultValue);
  1. Add a context provider: Add a context provider at the appropriate place in the component tree. The provider accepts a value prop, which will be passed to consumers.
<MyContext.Provider value={/* shared value */}>
  {/* children components */}
</MyContext.Provider>
  1. Use context in child components: In any lower-level component in the tree, you can use the useContext Hook or the context consumer component to access the context value.
// Using useContext Hook
import React, { useContext } from 'react';

function MyComponent() {
  const contextValue = useContext(MyContext);
  // ...
}

// Using Context.Consumer component
import React from 'react';

function MyComponent() {
  return (
    <MyContext.Consumer>
      {contextValue => {
        // ...
      }}
    </MyContext.Consumer>
  );
}

By using the Context API, you can easily share state at any point in the component tree without having to pass props down through every level. This makes state sharing between components that span multiple levels more concise and efficient. However, it is important to note that overusing context can lead to tight coupling between components, which can reduce code maintainability. Therefore, when using the Context API, ensure that it is used in scenarios where global state sharing is genuinely needed.

4. What is the jotai library, and how does it help manage application state?#

Jotai is a lightweight state management library designed for React applications. It is based on the concepts of atoms and selectors, making state management simple and efficient. The core idea of Jotai is to break down state into the smallest, composable units (atoms), making it easier to manage and track state. Compared to other state management libraries like Redux or MobX, Jotai is lighter and easier to learn.

Using Jotai has advantages over using the Context API in terms of simplicity, flexibility, and maintainability. Here are some reasons:

  1. Simple and easy to use.
    Using Jotai only requires creating atoms and using React Hooks for state management. The code for using Jotai is simpler and easier to use compared to the Context API.

  2. Highly flexible.
    Jotai allows you to freely combine and compose different atoms to create more complex states, making state management more flexible and scalable. The flexibility of using Jotai is higher than that of the Context API.

  3. Better performance.
    Using Jotai can avoid unnecessary renders caused by using Provider and Consumer components in the Context API, thus improving application performance. Jotai automatically optimizes component re-renders and only updates related components when atom states change.

  4. Easier to maintain.
    Using Jotai can make state management clearer, more explicit, and easier to maintain. By breaking down state into multiple atoms, each atom containing a single state value, you can better control state changes and maintain application state.

Using Jotai can make state management simpler, more flexible, easier to maintain, and perform better. Of course, the Context API can also be used for state management and is more native, but handling complex states may require more code and can easily lead to performance issues. Therefore, when choosing a state management library, it is essential to consider the specific situation.

Data Transmission and Processing#

1. How to pass data (props) between React components?#

  1. Passing data from parent component to child component

When using a child component in a parent component, you can pass data by adding props to the child component. For example:

function Parent() {
  const data = {name: 'John', age: 30};
  return <Child data={data} />;
}

function Child(props) {
  return (
    <div>
      <p>Name: {props.data.name}</p>
      <p>Age: {props.data.age}</p>
    </div>
  );
}

In this example, the parent component Parent passes an object named data to the child component Child, which can access this object via props.data.

  1. Passing data from child component to parent component

In the child component, you can pass data to the parent component by calling a function passed from the parent component. For example:

function Parent() {
  function handleChildData(data) {
    console.log(data);
  }

  return <Child onData={handleChildData} />;
}

function Child(props) {
  function handleClick() {
    props.onData('Hello, parent!');
  }

  return <button onClick={handleClick}>Click me</button>;
}

In this example, the child component Child passes data to the parent component by calling the props.onData function.

  1. Passing data between sibling components

To pass data between sibling components, you can define state in their common parent component and then pass that state as props to them. For example:

function Parent() {
  const [data, setData] = useState('Hello, world!');

  return (
    <>
      <Sibling1 data={data} />
      <Sibling2 setData={setData} />
    </>
  );
}

function Sibling1(props) {
  return <p>{props.data}</p>;
}

function Sibling2(props) {
  function handleClick() {
    props.setData('Hello, sibling 1!');
  }

  return <button onClick={handleClick}>Click me</button>;
}

In this example, Sibling1 and Sibling2 are sibling components that communicate through the state data in their common parent component Parent. Sibling1 retrieves the data via props.data, while Sibling2 updates the data using props.setData.

2. What is the "lift state up" pattern in React, and why is it important for data transmission and processing?#

"Lifting state up" is a common pattern in React used for handling data transmission and state management between components. The main idea of this pattern is to lift the shared state between components up to their common parent component for management, allowing for better management and coordination of data flow between components.

By lifting state to a common parent component, you can pass the state as props to child components, thus sharing data between them. This can make data transmission between components clearer and more intuitive, avoiding issues of components depending on each other and modifying each other's state. Additionally, this pattern can reduce duplicate state management code, making the code cleaner and easier to maintain.

The "lift state up" pattern is very important for handling data transmission and state management between components in React. In React, data transmission between components is typically achieved through props. When components need to access shared state, these states can be lifted to their common parent component for management, and the state can be passed as props to child components. This pattern can make data transmission between components clearer and more intuitive, avoiding issues of components depending on each other and modifying each other's state.

Moreover, the "lift state up" pattern can also make the code more reliable and maintainable. By managing state in a common parent component, you can reduce duplicate state management code and encapsulate state logic within the parent component, making the code cleaner and easier to maintain.

3. How to handle asynchronous data loading and updates (e.g., fetching data from an API)?#

  1. Using the useEffect Hook
    You can use the useEffect Hook to handle asynchronous data loading and updates. Inside useEffect, you can use an asynchronous function to fetch data and use the useState Hook to store the data and update the state. For example:
import { useEffect, useState } from 'react';

function App() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      setData(data);
    }

    fetchData();
  }, []);

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

In this example, we use the useEffect Hook to asynchronously fetch data and use the useState Hook to store and update the data. The second parameter of useEffect is an empty array, indicating that it should only execute once when the component mounts.

  1. Using event callbacks

You can handle asynchronous data loading and updates using event callbacks within the component. For example:

import { useState } from 'react';

function App() {
  const [data, setData] = useState([]);

  async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setData(data);
  }

  return (
    <div>
      <button onClick={fetchData}>Load data</button>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

In this example, we use an event callback to handle asynchronous data loading and updates. When the button is clicked, the fetchData function is called to fetch data again and store it in the state.

Which method to use depends on the specific needs of the scenario. Correctly distinguishing between Event and Effect is very important; you can refer to this document: separating-events-from-effects

4. What are controlled components and uncontrolled components? What are their application scenarios in data processing?#

Controlled components and uncontrolled components are concepts typically used for form elements (like input fields, select boxes, and radio buttons).

However, these concepts can also be extended to non-form element components. The key lies in how the internal state of the component is managed and how external data is processed. Here is a simple example illustrating how to apply the concepts of controlled and uncontrolled components to non-form elements:

Controlled Component (non-form element):

import React from 'react';

function ControlledDiv({ content, onContentChange }) {
  const handleClick = () => {
    onContentChange('New Content');
  };

  return <div onClick={handleClick}>{content}</div>;
}

In this example, the ControlledDiv component receives a content prop and an onContentChange callback function. When the user clicks this component, it triggers the callback function to update the externally passed content. This means the internal state of the component is controlled by external factors, so it can be considered a controlled component.

Uncontrolled Component (non-form element):

import React, { useState } from 'react';

function UncontrolledDiv() {
  const [content, setContent] = useState('Initial Content');

  const handleClick = () => {
    setContent('New Content');
  };

  return <div onClick={handleClick}>{content}</div>;
}

In this example, the UncontrolledDiv component maintains an internal content state. When the user clicks this component, it directly updates the internal state without needing to fetch data from external sources. Therefore, this component can be considered an uncontrolled component.

In summary, although the concepts of controlled and uncontrolled components primarily apply to form elements, they can actually be extended to non-form elements as well, with the key being how the component's state is managed and how data is processed.

5. How to use React's useCallback and useMemo Hooks to optimize data processing and function passing?#

useCallback and useMemo are two Hooks in React that can help optimize data processing and function passing, avoiding unnecessary component re-renders. Here is an example using useMemo:

import { useMemo, memo, useState } from "react";

const ChildComponent = memo(function ChildComponent({ data }) {
  console.log("Child component render");
  return (
    <div>
      <p>Name: {data.name}</p>
      <p>Age: {data.age}</p>
    </div>
  );
});

function ParentComponent() {
  const [num, setNum] = useState(0);
  // Do not learn this example; do not use useMemo without performance issues
  const data = useMemo(() => {
    return { name: "John", age: 30 };
  }, []);

  return (
    <>
      <div>
        num: {num}{" "}
        <button onClick={() => setNum((_num) => _num + 1)}>increase</button>
      </div>
      <ChildComponent data={data} />
    </>
  );
}

export default ParentComponent;

codesandbox demo

In this example, ParentComponent uses useMemo to wrap an object and passes it as props to the memo-wrapped ChildComponent. Since ChildComponent is memo-wrapped, it will only re-render when data changes.

When we click the increase button, although ParentComponent re-renders, the reference of data remains unchanged due to useMemo, so ChildComponent will not re-render.

::: tip
Please note that ChildComponent must be a React.memo wrapped component for the optimization of useMemo to take effect.

This is because when ParentComponent re-renders, its child component will also re-render, regardless of whether its props have changed. Only when its child component is a React.memo component will React use Object.is to compare props for changes to decide whether to skip the re-render.
:::

6. How to leverage React's custom Hooks to encapsulate and reuse data processing logic?#

Custom Hooks are a way to encapsulate and reuse state and side effect logic in functional components. The naming of custom Hooks typically starts with "use." Here is a simple example of a custom Hook:

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

// Define a custom Hook for encapsulating data processing logic
function useDataHandling(data) {
  const [processedData, setProcessedData] = useState(null);

  useEffect(() => {
    // Define data processing logic
    function processData(data) {
      // ... data processing steps ...
      return processedData;
    }

    // Process data and update state
    setProcessedData(processData(data));
  }, [data]);

  // Return processed data
  return processedData;
}

// Use the custom Hook in a functional component
function MyComponent({ data }) {
  const processedData = useDataHandling(data);
  // ... use the processed data ...
}

Custom Hooks can help make code more modular and clear. Even without considering code reuse, splitting logic into custom Hooks still has certain advantages:

  1. Separation of concerns: Custom Hooks can separate different concerns (like state management, side effect handling, data processing, etc.) into different Hooks. This helps make component code cleaner and easier to understand and maintain.
  2. Decoupling logic: Encapsulating specific logic into a custom Hook can reduce the coupling between components, making them more flexible. This way, when requirements change, modifying the custom Hook will not affect other components.
  3. Easier testing: Custom Hooks can be tested independently of components. This means you can write unit tests for specific logic without worrying about the influence of other components.
  4. Better readability: Using custom Hooks can make component code more descriptive, as the names of Hooks often reflect their functionality and purpose. This helps improve code readability and maintainability.

Therefore, in actual development, even if a piece of code will not be reused, it is beneficial to split it into a custom Hook. When refactoring code, consider splitting logic into appropriate custom Hooks to improve code quality.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.