MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 数据获取与 useEffect 的最佳实践

2021-09-215.1k 阅读

1. React 中的数据获取概述

在 React 应用开发中,数据获取是一项核心任务。无论是从后端 API 加载用户信息、产品列表,还是从本地存储读取配置数据,都涉及到数据获取的操作。在 React 早期,数据获取通常在 componentDidMountcomponentDidUpdate 等生命周期方法中进行。随着 React Hooks 的出现,useEffect 钩子函数成为了处理副作用(包括数据获取)的首选方式。

1.1 传统生命周期中的数据获取

在类组件中,我们常使用 componentDidMount 来发起数据请求,因为这个方法在组件挂载到 DOM 后只执行一次,非常适合初始化数据获取。例如:

import React, { Component } from 'react';

class DataFetching extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      error: null
    };
  }

  componentDidMount() {
    fetch('https://example.com/api/data')
    .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
    .then(data => this.setState({ data }))
    .catch(error => this.setState({ error }));
  }

  render() {
    const { data, error } = this.state;
    if (error) {
      return <div>Error: {error.message}</div>;
    }
    if (!data) {
      return <div>Loading...</div>;
    }
    return <div>{JSON.stringify(data)}</div>;
  }
}

export default DataFetching;

在上述代码中,componentDidMount 方法内发起了一个 fetch 请求,成功时更新 state 中的 data,出错时更新 state 中的 error。然后在 render 方法中根据 state 的不同情况进行相应渲染。

1.2 为什么转向 useEffect

虽然传统生命周期方法能完成数据获取任务,但它们存在一些问题。例如,在类组件中,多个生命周期方法可能会混杂不同的逻辑,使得代码难以维护和理解。而 useEffect 以一种更灵活、更简洁的方式处理副作用。useEffect 可以看作是 componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合,通过传入不同的依赖数组来控制其执行时机。

2. useEffect 基础

useEffect 是 React 提供的一个 Hook,用于在函数组件中执行副作用操作。副作用操作包括数据获取、订阅事件、手动操作 DOM 等。

2.1 useEffect 的基本语法

useEffect 接受两个参数:一个是副作用函数(包含需要执行的副作用操作),另一个是可选的依赖数组。

import React, { useEffect } from'react';

function MyComponent() {
  useEffect(() => {
    // 副作用函数
    console.log('Component mounted or updated');

    return () => {
      // 清理函数(可选),在组件卸载或依赖变化时执行
      console.log('Component will unmount or dependency changed');
    };
  }, []); // 依赖数组

  return <div>My Component</div>;
}

export default MyComponent;

在上述代码中,useEffect 内的副作用函数在组件挂载后执行,并且由于依赖数组为空,它不会在组件更新时再次执行。如果依赖数组不为空,例如 useEffect(() => { /*... */ }, [someVariable]),则只有当 someVariable 的值发生变化时,副作用函数才会重新执行。

2.2 清理函数的作用

清理函数用于在组件卸载或依赖变化时执行一些清理操作。例如,当我们在副作用函数中订阅了一个事件,在组件卸载时需要取消订阅,以避免内存泄漏。

import React, { useEffect } from'react';

function EventSubscriber() {
  useEffect(() => {
    const handleClick = () => {
      console.log('Button clicked');
    };
    document.addEventListener('click', handleClick);

    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, []);

  return <div>Event Subscriber</div>;
}

export default EventSubscriber;

在这个例子中,addEventListener 添加了一个点击事件监听器,清理函数 removeEventListener 在组件卸载时移除该监听器。

3. 使用 useEffect 进行数据获取

3.1 简单的数据获取

使用 useEffect 进行数据获取非常直观。我们可以在副作用函数中发起 fetch 请求,并更新组件的状态。

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

function DataFetching() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://example.com/api/data')
    .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
    .then(data => setData(data))
    .catch(error => setError(error));
  }, []);

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  if (!data) {
    return <div>Loading...</div>;
  }
  return <div>{JSON.stringify(data)}</div>;
}

export default DataFetching;

这里我们使用 useState 来管理数据和错误状态。useEffect 的依赖数组为空,确保数据请求只在组件挂载时执行一次。

3.2 依赖变化时的数据获取

有时候,我们需要根据组件的某个属性或状态变化来重新获取数据。例如,当用户选择不同的分类时,我们需要加载该分类对应的产品数据。

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

function ProductList({ category }) {
  const [products, setProducts] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await fetch(`https://example.com/api/products?category=${category}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setProducts(data);
      } catch (error) {
        setError(error);
      }
    };
    fetchProducts();
  }, [category]);

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  if (!products) {
    return <div>Loading...</div>;
  }
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

export default ProductList;

在这个例子中,useEffect 的依赖数组包含 category。当 category 发生变化时,useEffect 内的副作用函数会重新执行,从而发起新的数据请求。

3.3 多次数据获取

在一些复杂的场景中,组件可能需要进行多次数据获取。例如,获取用户信息后,根据用户信息再获取其订单列表。

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

function UserOrderList() {
  const [user, setUser] = useState(null);
  const [orders, setOrders] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch('https://example.com/api/user');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (error) {
        setError(error);
      }
    };
    fetchUser();
  }, []);

  useEffect(() => {
    if (user) {
      const fetchOrders = async () => {
        try {
          const response = await fetch(`https://example.com/api/orders?userId=${user.id}`);
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          const data = await response.json();
          setOrders(data);
        } catch (error) {
          setError(error);
        }
      };
      fetchOrders();
    }
  }, [user]);

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  if (!user ||!orders) {
    return <div>Loading...</div>;
  }
  return (
    <div>
      <h2>{user.name}'s Orders</h2>
      <ul>
        {orders.map(order => (
          <li key={order.id}>{order.product}</li>
        ))}
      </ul>
    </div>
  );
}

export default UserOrderList;

这里我们使用了两个 useEffect。第一个 useEffect 在组件挂载时获取用户信息,第二个 useEffectuser 状态更新后获取该用户的订单列表。

4. 数据获取的优化

4.1 防抖与节流

在数据获取过程中,如果依赖频繁变化,可能会导致不必要的多次请求。防抖(Debounce)和节流(Throttle)技术可以解决这个问题。

防抖是指在一定时间内,多次触发事件只执行一次。例如,当用户在搜索框中输入时,我们不希望每次输入都发起搜索请求,而是在用户停止输入一段时间后再发起请求。

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

function DebouncedSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      if (searchTerm) {
        fetch(`https://example.com/api/search?query=${searchTerm}`)
        .then(response => {
            if (!response.ok) {
              throw new Error('Network response was not ok');
            }
            return response.json();
          })
        .then(data => setResults(data))
        .catch(error => setError(error));
      }
    }, 300);

    return () => clearTimeout(timeoutId);
  }, [searchTerm]);

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  if (!results) {
    return <div>Loading...</div>;
  }
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default DebouncedSearch;

在这个例子中,setTimeout 设置了 300 毫秒的延迟,只有在 300 毫秒内 searchTerm 没有再次变化时,才会发起数据请求。清理函数 clearTimeout 在组件卸载或 searchTerm 变化时清除定时器,避免内存泄漏。

节流则是指在一定时间内,只允许事件触发一次。例如,用户频繁点击加载更多按钮,我们可以使用节流确保每 1 秒只发起一次加载更多的请求。

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

function ThrottledLoadMore() {
  const [page, setPage] = useState(1);
  const [data, setData] = useState([]);
  const [error, setError] = useState(null);
  let throttleTimer = null;

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(`https://example.com/api/data?page=${page}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const newData = await response.json();
        setData([...data, ...newData]);
      } catch (error) {
        setError(error);
      }
    };

    if (!throttleTimer) {
      fetchData();
      throttleTimer = setTimeout(() => {
        throttleTimer = null;
      }, 1000);
    }
  }, [page]);

  const loadMore = () => {
    setPage(page + 1);
  };

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  return (
    <div>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <button onClick={loadMore}>Load More</button>
    </div>
  );
}

export default ThrottledLoadMore;

在这个例子中,throttleTimer 用于控制节流,每 1 秒内只允许发起一次数据请求。

4.2 缓存数据

在多次数据获取相同数据的情况下,缓存数据可以避免重复请求,提高性能。我们可以使用一个简单的对象来缓存数据。

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

const dataCache = {};

function CachedDataFetching() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (dataCache['https://example.com/api/data']) {
      setData(dataCache['https://example.com/api/data']);
    } else {
      fetch('https://example.com/api/data')
      .then(response => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
      .then(data => {
          dataCache['https://example.com/api/data'] = data;
          setData(data);
        })
      .catch(error => setError(error));
    }
  }, []);

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  if (!data) {
    return <div>Loading...</div>;
  }
  return <div>{JSON.stringify(data)}</div>;
}

export default CachedDataFetching;

在这个例子中,dataCache 对象用于缓存数据。如果缓存中已经有数据,则直接使用缓存数据,否则发起请求并将结果存入缓存。

4.3 错误处理与重试

在数据获取过程中,难免会遇到网络错误等问题。我们可以在错误处理时添加重试机制。

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

function RetryDataFetching() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [retryCount, setRetryCount] = useState(0);
  const maxRetries = 3;

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://example.com/api/data');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        if (retryCount < maxRetries) {
          setRetryCount(retryCount + 1);
          setTimeout(() => {
            fetchData();
          }, 1000 * retryCount);
        } else {
          setError(error);
        }
      }
    };
    fetchData();
  }, [retryCount]);

  if (error) {
    return <div>Error: {error.message}</div>;
  }
  if (!data) {
    return <div>Loading...</div>;
  }
  return <div>{JSON.stringify(data)}</div>;
}

export default RetryDataFetching;

在这个例子中,当数据获取失败时,如果重试次数小于 maxRetries,则等待一段时间后重试,每次重试的等待时间逐渐增加。

5. 结合 Redux 进行数据获取

Redux 是一个流行的状态管理库,与 React 结合使用可以更好地管理应用的状态,特别是在数据获取方面。

5.1 Redux 中的数据获取流程

在 Redux 中,数据获取通常涉及以下几个步骤:

  1. 发起 Action:组件通过 dispatch 一个 action 来触发数据获取。例如,dispatch({ type: 'FETCH_DATA_REQUEST' })
  2. Reducer 处理 Action:Reducer 根据接收到的 action 更新状态。在 FETCH_DATA_REQUEST action 时,可能会将 loading 状态设为 true
  3. 异步操作:使用中间件(如 redux - thunkredux - saga)来处理异步数据获取。例如,在 redux - thunk 中,action creator 可以返回一个函数,在函数中发起 fetch 请求。
  4. 更新状态:请求成功或失败时,dispatch 相应的 action(如 FETCH_DATA_SUCCESSFETCH_DATA_FAILURE),Reducer 根据这些 action 更新数据和错误状态。

5.2 示例代码

首先,安装必要的库:

npm install redux react - redux redux - thunk

然后,创建 Redux 的相关文件。

actions/dataActions.js

import { FETCH_DATA_REQUEST, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE } from './actionTypes';
import axios from 'axios';

export const fetchDataRequest = () => ({
  type: FETCH_DATA_REQUEST
});

export const fetchDataSuccess = data => ({
  type: FETCH_DATA_SUCCESS,
  payload: data
});

export const fetchDataFailure = error => ({
  type: FETCH_DATA_FAILURE,
  payload: error
});

export const fetchData = () => {
  return async dispatch => {
    dispatch(fetchDataRequest());
    try {
      const response = await axios.get('https://example.com/api/data');
      dispatch(fetchDataSuccess(response.data));
    } catch (error) {
      dispatch(fetchDataFailure(error.message));
    }
  };
};

reducers/dataReducer.js

import { FETCH_DATA_REQUEST, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE } from '../actions/actionTypes';

const initialState = {
  data: null,
  loading: false,
  error: null
};

const dataReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_DATA_REQUEST:
      return {
      ...state,
        loading: true
      };
    case FETCH_DATA_SUCCESS:
      return {
      ...state,
        loading: false,
        data: action.payload,
        error: null
      };
    case FETCH_DATA_FAILURE:
      return {
      ...state,
        loading: false,
        data: null,
        error: action.payload
      };
    default:
      return state;
  }
};

export default dataReducer;

actionTypes.js

export const FETCH_DATA_REQUEST = 'FETCH_DATA_REQUEST';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAILURE = 'FETCH_DATA_FAILURE';

store.js

import { createStore, applyMiddleware } from'redux';
import thunk from'redux - thunk';
import dataReducer from './reducers/dataReducer';

const store = createStore(dataReducer, applyMiddleware(thunk));

export default store;

App.js

import React from'react';
import { Provider } from'react - redux';
import store from './store';
import DataFetchingComponent from './components/DataFetchingComponent';

function App() {
  return (
    <Provider store={store}>
      <DataFetchingComponent />
    </Provider>
  );
}

export default App;

components/DataFetchingComponent.js

import React from'react';
import { useSelector, useDispatch } from'react - redux';
import { fetchData } from '../actions/dataActions';

function DataFetchingComponent() {
  const data = useSelector(state => state.data.data);
  const loading = useSelector(state => state.data.loading);
  const error = useSelector(state => state.data.error);
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);

  if (loading) {
    return <div>Loading...</div>;
  }
  if (error) {
    return <div>Error: {error}</div>;
  }
  if (!data) {
    return null;
  }
  return <div>{JSON.stringify(data)}</div>;
}

export default DataFetchingComponent;

在这个例子中,我们通过 Redux 管理数据获取的状态,DataFetchingComponent 组件通过 useSelector 获取 Redux 状态,通过 useDispatch 触发数据获取 action。

6. 与 GraphQL 的结合

GraphQL 是一种用于 API 的查询语言,与 React 结合使用可以更高效地获取数据。

6.1 Apollo Client 简介

Apollo Client 是一个流行的 GraphQL 客户端,用于在 React 应用中管理 GraphQL 数据。它提供了缓存、查询管理、自动更新等功能。

6.2 配置 Apollo Client

首先,安装必要的库:

npm install @apollo/client graphql

然后,创建 Apollo Client 实例。

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://example.com/graphql',
  cache: new InMemoryCache()
});

export default client;

6.3 使用 Apollo Client 进行数据获取

在 React 组件中,可以使用 useQuery Hook 来执行 GraphQL 查询。

import React from'react';
import { useQuery } from '@apollo/client';
import gql from 'graphql-tag';

const GET_DATA = gql`
  query GetData {
    data {
      id
      name
    }
  }
`;

function GraphQLDataFetching() {
  const { loading, error, data } = useQuery(GET_DATA);

  if (loading) {
    return <div>Loading...</div>;
  }
  if (error) {
    return <div>Error: {error.message}</div>;
  }
  return (
    <ul>
      {data.data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

export default GraphQLDataFetching;

在这个例子中,useQuery 执行了 GET_DATA 查询,并根据查询结果的状态进行相应渲染。Apollo Client 会自动处理缓存,避免不必要的重复查询。

通过以上内容,我们详细探讨了 React 中使用 useEffect 进行数据获取的各种场景、优化方法以及与 Redux、GraphQL 的结合使用,希望能帮助开发者在实际项目中更高效地处理数据获取任务。