如何通过 api 使用增量更新 React 组件

问题描述 投票:0回答:1

我有一个 React 应用程序(由 Next.js 提供支持,并在 TypeScript 中实现),其中一个组件中包含一个由 api 支持的数据集。该组件的 api 数据可以无限期地缓存,因为它是只读数据集,永远不会在服务器上更改。网上有很多关于如何准确执行此操作的好示例,以及更复杂的完整 CRUD 应用程序。这些应用程序在服务器和客户端上维护一组数据,使它们在两者之间保持同步。对于应该从服务器呈现完整数据集的应用程序来说,这项工作很好。我还看到了当服务器数据集很大时进行分页的示例。

我现在正在尝试构建一个组件,其功能需要与我迄今为止找到的示例略有不同:

  1. 启动时仅从服务器获取最近 100 条消息,并渲染它们
  2. 定期轮询 api 以获取自消息 M 以来的下 N 条消息(其中 M 是 Web 应用程序拥有的最新消息)
  3. 当轮询的 api 产生消息时,将这些添加到渲染消息集。

在示例中,我看到重新渲染反应组件(在新数据上)的触发器似乎要求该组件是从状态变量数组构建的,该数组本身在获取器内设置(覆盖)。我认为我不能在那里使用状态变量,因为 useState 实现了相应的 set* 方法来简单地覆盖状态变量的数据,而不是添加它。我最初尝试使用两个变量:一个状态变量填充有新消息,另一个状态变量包含接收到的数据的超集,但是我没有看到在更新其构建的状态变量后重新渲染反应组件来自(packetsToRender)。

我当前的代码如下:

React 组件代码

import { useState, useEffect } from 'react';

var packets: PacketSummary[] = [];
var latestPacketId: number = -1;

function CommsPacketList({}: {}) {
    const rows: any[] = [];
    const [fetchError, setFetchError] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [packetsToRender, setPacketsToRender] = useState(packets);
    var url: string;

    function addPacketsToArray(newPackets: PacketSummary[]) {
        var lastId = -1;
        if (newPackets.length == 0) return
        newPackets.forEach((packetSummary: PacketSummary) => {
            packets.push(packetSummary);
            lastId = packetSummary.packet_index;
        });
        latestPacketId = lastId;
        setPacketsToRender(packets);
        console.log('Added packets to array. latestPacketId = ', lastId);
    }
    
    useEffect(() => {
        const fetchNewItems = async () => {
            try {
                url = API_ENDPOINT__COMMS_PACKET_LIST + '?limit=100&offset_index=' + latestPacketId;
                const response = await fetch(url);
                const data = await response.json();
                addPacketsToArray(data);
                setFetchError(null);
            } catch (error: any) {
                setFetchError(error.message);
            } finally {
                setIsLoading(false);
            }

            setTimeout(() => {
                fetchNewItems();
            }, 5000);    
        }

        (async () => await fetchNewItems())();
    }, []);

    if (!packetsToRender && fetchError) return <div>Failed to load. Error: {fetchError}  </div>
    if (!packetsToRender && isLoading) return <div>Loading...</div>
    if (!packetsToRender) return <div>No packets!</div>

    packetsToRender.forEach((packetSummary: PacketSummary) => {
        rows.push(
            <MessagePacket
                packetSummary={packetSummary} 
            />
        );
    });
    return (
        <div className={styles.comms_packet_listing}>
            {rows}
        </div>
    );
}

使用此代码我遇到以下问题:

  1. 组件仅渲染一次 - 第一次 api 响应包含一些消息时。
    packetsToRender
    addPacketsToArray
    的所有后续更新不会产生任何组件重新渲染,因此只有第一条消息可见。 (我添加了 console.log 输出来验证
    packets
    addPacketsToArray
    是否正在更新,并且
    packetsToRender.forEach...
    没有在初始加载后被调用)
  2. 所有 api 请求都是重复的:网络面板显示带有
    offset_index=-1
    的初始请求被发出两次,所有后续请求也是以 5 秒的间隔发出。
  3. 我正在使用
    setTimeout
    来实现轮询,但我想知道这是否是一种可行的方法?在获取器内部执行此操作似乎有点老套,只是对那里的良好实践感到好奇(!)
  4. 我在主消息数组中使用全局变量(
    packets
    lastPacketId
    ),我的代码编辑器告诉我,TypeScript 领域对此不以为然(我对此同样很陌生——再说一次,这是一个“良好实践”问题)

关于

setTimeout
的替代方案:我已经研究过axios和useSWR,但还没有找到任何可以帮助我的东西。 (useSWR 能够设置
refreshInterval
,但在我的测试中,我发现我无法以超过每秒一次的速度驱动更新 - 而我需要更新运行得更快一点 - 目标是每 0.1 秒)

reactjs node.js next.js react-hooks react-typescript
1个回答
0
投票

在使用生成式 AI 进行进一步迭代后,我能够解决上述问题(以及 Wiktor 提供的非常有用的提示 - 谢谢!)

  1. 用自定义的“useInterval”挂钩替换 setTimeout(如下所示)。使用 setTimeout 导致我的 api 请求加倍,并且在渲染中没有效果
  2. 我将 useEffect 的依赖项列表留空,因此该效果仅被调用一次。但是,我现在放弃了 useEffect,转而只使用自定义的“useInterval”挂钩。
  3. 正如 Wiktor 提到的,只需将增量添加到 packetToRender 数组中,然后再次用它设置状态变量
function useInterval(callback: () => void, delay: number | null) {
    const savedCallback = React.useRef<() => void>();

    React.useEffect(() => {
        savedCallback.current = callback;
    }, [callback]);

    React.useEffect(() => {
        function tick() {
        savedCallback.current?.();
        }
        if (delay !== null) {
        const intervalId = setInterval(tick, delay);
        return () => clearInterval(intervalId);
        }
    }, [delay]);
}

function CommsPacketList({}: {}) {
    const rows: any[] = [];
    var packets: PacketSummary[] = [];
    var url: string;

    const [fetchError, setFetchError] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [packetsToRender, setPacketsToRender] = useState(packets);
    const [latestPacketId, setLatestPacketId] = useState<number>(-1);    

    const addPacketsToArray = (newPackets: PacketSummary[]) => {
        if (!newPackets.length) return
        const updatedPackets = [...packetsToRender, ...newPackets];        
        const lastId = newPackets.length > 0 ? newPackets[newPackets.length - 1].packet_index : latestPacketId;
        setLatestPacketId(lastId);
        setPacketsToRender(updatedPackets);
    }
    
    const fetchNewItems = async () => {
        try {
            url = API_ENDPOINT__COMMS_PACKET_LIST + '?limit=100&offset_index=' + latestPacketId;
            const response = await fetch(url);
            const data = await response.json();
            addPacketsToArray(data);
            setFetchError(null);
        } catch (error: any) {
            setFetchError(error.message);
        } finally {
            setIsLoading(false);
        }
    }

    // Use the custom useInterval hook
    useInterval(() => {
        fetchNewItems();
    }, 200);    

    // Handle no packets case
    if (!packetsToRender && fetchError) return <div>Failed to load. Error: {fetchError}  </div>
    if (!packetsToRender && isLoading) return <div>Loading...</div>
    if (!packetsToRender) return <div>No packets!</div>

    // Render packets
    packetsToRender.forEach((packetSummary: PacketSummary) => {
        rows.push(
            <MessagePacket
                packetSummary={packetSummary} 
            />
        );
    });
    return (
        <div className={styles.comms_packet_listing}>
            {rows}
        </div>
    );
}

我现在已经能够将间隔减少到 200 毫秒,一切都按预期进行。

© www.soinside.com 2019 - 2024. All rights reserved.