App is stuck in a loop when rendering from an external API call

Hi,

I am trying to have an app that dynamically shows information from calling a service whenever a new cell is selected. This is working, but it seems to be stuck in an endless loop and continually re-renders even when the selection is not changing.

Any React guru who knows a better way please chime in!

import React, { useState, useEffect } from 'react';
import {
    initializeBlock,
    useBase,
    useRecords,
    useGlobalConfig,
    useSession,
    useLoadable, 
    useWatchable,
    Heading,
    Box
} from '@airtable/blocks/ui';
import { cursor } from '@airtable/blocks';
import ConfigField from './components/ConfigField.js'

const _ = require('lodash')
const axios = require('axios')

function UpdateAsin() {

    const base = useBase();
    useLoadable(cursor);
    useWatchable(cursor, ['selectedRecordIds', 'selectedFieldIds', 'activeTableId', 'activeViewId']);
    const table = base.getTableByIdIfExists(cursor.activeTableId)
    const globalConfig = useGlobalConfig();
    const configFields = {
        upc: new ConfigField('upc', 'upcFieldId', table)
    }
    const field = configFields.upc.field
    const ui = _.map(configFields, 'ui')

    return (
        <div>
            <Config fields={ui} gc={globalConfig} env={ui} />
            <Box padding="1rem">
                <ShowMwsData table={table}
                    field={field}
                    selectedRecordIds={cursor.selectedRecordIds}
                />
            </Box>
        </div>
    );
}

function ShowMwsData({ table, field, selectedRecordIds }) {
    if (!selectedRecordIds || selectedRecordIds.length == 0) {
        return (
            <Heading>Please select 1 or more records {selectedRecordIds ? selectedRecordIds.length : 'null'} </Heading>
        )
    }
    const selectedRecordIdsSet = new Set(selectedRecordIds);
    const records = useRecords(table, { fields: [field] });
    const selectedRecords = records.filter(record => selectedRecordIdsSet.has(record.id));
    const r = selectedRecords[0]
    return (
        <Box>
            <Heading>{selectedRecords.length} selected</Heading>
            <ShowSingle key={r.id} upc={r.getCellValueAsString(field)} />            
        </Box>
    )
}

function ShowSingle({ upc }) {
    const [mws, setMws] = useState({})

    axios.get(`http://localhost:8080/?upc=${upc}`)
        .then((response) => {
            setMws(response.data)
        })

    if (Object.keys(mws).length === 0) {
        return (<div></div>)
    }

    return (
        <Box>
            {
               JSON.stringify(mws, null, 4)
            }
        </Box>
    )
}

function Config({ table, fields, gc, env }) {

    return (
        <Box padding={3} borderBottom="thick">
            {fields}
        </Box>
    )
}
initializeBlock(() => <UpdateAsin />);

Right now, your external call is in the main render function for ShowSingle component, which will re-render whenever mws changes, resulting in the infinite loop you’re seeing.

To fix this, you’ll only want to trigger this request when the upc changes. You can accomplish that using a React useEffect hook, and can add extra validation using a custom usePrevious hook to store the last value of upc. Something like this…

// pulled from https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function ShowSingle({ upc }) {
    const [mws, setMws] = useState({})
    const prevUpc = usePrevious(upc)

    useEffect(() => {
        if (upc !== prevUpc) {
            axios.get(`http://localhost:8080/?upc=${upc}`)
                .then((response) => {
                    setMws(response.data)
                });
        }
    }, [setMws, prevUpc, upc])

    // ...rest of code
}

Excellent! Works Great!

I come from a Vue background where this is a little more magical. I had tried useEffect() but was missing useRef().