返回

React Context API初始化:异步数据获取最佳实践

javascript

从API初始化上下文:解决方案探讨

在React应用开发中,我们经常使用Context来跨组件共享数据。一个常见需求是从外部API获取数据,然后用这些数据初始化Context。直接在模块顶级作用域中使用 async/awaitfetch,如提供的示例代码,会产生错误。 JavaScript模块加载是同步的,它无法等待异步操作完成。我们需要找到正确的方法,来解决这一难题。

方案一:使用useEffect和状态管理

这种方式较为直接,在 UsuariosProvider 组件内部,使用 useState 管理数据, useEffect 副作用钩子负责从API获取数据,并在数据获取后更新状态。

原理: useEffect 可以在组件渲染之后执行副作用操作。通过将 [] 作为 useEffect 的第二个参数传入,它只会在组件挂载时执行一次。我们在此获取数据,并将获取的数据设置到状态中。Context Provider 始终从状态变量 usuarios 读取数据,实现了API数据的初始化。

代码示例:

import React, { createContext, useState, useEffect } from "react";
import { Usuario } from "../typings/types";


interface UsuariosContextType {
    usuarios: Usuario[]
    setUsuario?: React.Dispatch<React.SetStateAction<Usuario[]>>;
}

export const UsuariosContext = createContext<UsuariosContextType>({ usuarios: [] })

export const UsuariosProvider = ({ children }: { children: React.ReactNode }) => {
    const [usuarios, setUsuario] = useState<Usuario[]>([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const fetchData = async () => {
            try {
              const response = await fetch(`http://localhost:8080/usuarios`, { cache: 'no-store' });
              if(!response.ok){
                throw new Error(`HTTP Error: ${response.status}`);
              }
              const data = await response.json();
              setUsuario(data);
              } catch (error){
                 console.error("Fetch Error:",error)
              } finally {
                  setLoading(false);
              }

        };
        fetchData();
    }, []);


    if (loading){
        return <div> Loading ... </div>
    }
    return (
        <UsuariosContext.Provider value={{ usuarios, setUsuario }}>
            {children}
        </UsuariosContext.Provider >
    )
}

步骤:

  1. 定义 loading 状态变量,控制加载时的渲染行为。
  2. 使用 useEffect 获取 API 数据。
  3. 在成功获取数据后,更新 usuarios 状态。
  4. 加入错误处理机制,增加应用的鲁棒性。
  5. 在组件返回之前增加对 loading 状态的检查,如果 loading 为真则显示加载提示。

注意事项:
添加合适的加载状态(如上代码)非常重要。 否则,Context 在初始化时没有数据,使用该 Context 的组件在第一次渲染时会访问到空数据。

方案二: 使用异步初始化 Context Provider

将 Context Provider 设置为一个异步函数,在该异步函数内完成 API 的请求操作。当 API 数据加载完成后再返回组件。 这种方式利用了 React 组件异步渲染的能力。

原理: 利用异步函数完成初始化数据获取,之后再进行组件的渲染,通过判断是否有初始化完成的变量来进行有条件渲染。

代码示例:

import React, { createContext, useState, useEffect, FC} from "react";
import { Usuario } from "../typings/types";


interface UsuariosContextType {
    usuarios: Usuario[]
    setUsuario?: React.Dispatch<React.SetStateAction<Usuario[]>>;
}

export const UsuariosContext = createContext<UsuariosContextType>({ usuarios: [] });
interface UsuariosProviderProps {
    children: React.ReactNode
}


export const UsuariosProvider: FC<UsuariosProviderProps> = ({ children })=>{
  const [usuarios, setUsuarios] = useState<Usuario[]>([]);
  const [isInit,setIsInit] = useState<boolean>(false);
  
   useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("http://localhost:8080/usuarios", { cache: 'no-store' });
        if(!response.ok){
           throw new Error(`Http error: ${response.status}`);
        }
          const data = await response.json();
          setUsuarios(data);
          setIsInit(true);
       } catch (error){
           console.error("fetch Error: ",error);
           setIsInit(true)
        }
      
      };
     fetchData();
  }, [])

  if (!isInit) {
    return <div>Initializing ...</div>;
  }
  
  return (
        <UsuariosContext.Provider value={{ usuarios, setUsuario: setUsuarios}}>
          {children}
      </UsuariosContext.Provider>
  );
}

步骤:

  1. 添加 isInit 状态值来判断组件初始化状态。
  2. fetch 请求放在一个 useEffect 函数中。
  3. 使用 isInit 作为是否显示加载动画的依据,没有初始化成功不显示页面内容。
  4. 捕获和记录可能出现的错误信息。

注意事项:
需要仔细控制加载状态,避免不必要的空白渲染或报错,对数据进行严格的校验,增强应用的稳定性。

方案三: 引入缓存机制

在某些场景下,可以考虑引入缓存机制。第一次请求数据后,可以将数据存储在浏览器的 localStorage 中,后续初始化时,先读取缓存数据。 当缓存数据不存在或者过期的时候,才去重新请求数据。

原理: 减少对API的请求,提升应用加载速度,同时还能在API服务暂时不可用的时候,应用可以读取缓存。

代码示例:

import React, { createContext, useState, useEffect } from "react";
import { Usuario } from "../typings/types";

interface UsuariosContextType {
    usuarios: Usuario[]
    setUsuario?: React.Dispatch<React.SetStateAction<Usuario[]>>;
}

export const UsuariosContext = createContext<UsuariosContextType>({ usuarios: [] })

const CACHE_KEY = "usuarios_cache";

export const UsuariosProvider = ({ children }: { children: React.ReactNode }) => {
  const [usuarios, setUsuarios] = useState<Usuario[]>([]);
    const [loading, setLoading] = useState(true);
  

  useEffect(() => {
      const fetchData = async () => {
          const cachedData = localStorage.getItem(CACHE_KEY);

          if(cachedData){
             try{
               setUsuarios(JSON.parse(cachedData));
              
               setLoading(false)
               return;
              } catch(e){
                //handle data parser error
              }
           }

          try {
              const response = await fetch("http://localhost:8080/usuarios", { cache: 'no-store' });
            if (!response.ok){
                throw new Error(`Http error: ${response.status}`);
             }
               const data = await response.json();
            setUsuarios(data);
              localStorage.setItem(CACHE_KEY, JSON.stringify(data));
              setLoading(false);

          } catch (error) {
              console.error("Fetch error:", error);
            }
            finally{
              setLoading(false);
           }
          };
    fetchData()
    },[])

    if (loading){
          return <div> Loading ... </div>
      }

  return (
          <UsuariosContext.Provider value={{ usuarios, setUsuario: setUsuarios}}>
              {children}
          </UsuariosContext.Provider>
  );
}

步骤:

  1. 定义 CACHE_KEY 用于指定存储的key值。
  2. 获取本地 localStorage 存储的缓存数据。
  3. 如果 localStorage 存在数据,使用存储的缓存数据初始化状态值并设置 loading 为false
  4. 否则, 发起fetch请求, 获取数据并且把数据更新到状态值,同时把最新的数据缓存到 localStorage

注意事项: 缓存策略需要考虑数据过期和同步问题。 对于更新频繁的数据,不能无限期缓存。 应该考虑数据更新机制,防止应用中出现“陈旧”数据。