ListView not refreshing UI after state updates in dynamic calculations

1) My goal: Create a dynamic invoice/order form where users can edit either net price or gross total, and all related values (net total, gross total, net price) are automatically recalculated based on quantity and VAT rate.

2) Issue:

  • State updates correctly - the ListViewData state shows the correct calculated values in the State tab
  • ListView UI doesn't refresh - the visual components (input fields) in the ListView don't update to show the new calculated values, even though the underlying state has changed
  • Intermittent behavior - sometimes calculations work on first edit, but subsequent edits either don't trigger or don't display properly
  1. Steps I've taken to troubleshoot:
  • Added proper error handling - wrapped code in try/catch blocks
  • Verified data binding - confirmed ListView data source is {{ ListViewData.value }}
  • Tried different update methods:
  • ListViewData.setValue(newArray)
  • ListViewData.setIn([index, field], value) for individual fields
  • Added forced refresh - used setTimeout with listView1.setData()
  • Verified event handler configuration - passing correct index and field parameters
  • Added extensive console logging - confirmed calculations are working and state is updating
  1. Additional info: (Cloud or Self-hosted, Screenshots)
  • Platform: Retool Cloud
  • Component: ListView with embedded input components
  • Data flow: User input → Event handler → JavaScript query → State update → ListView should refresh
  • Current behavior: State updates :white_check_mark:, ListView UI refresh :x:
  • Error patterns: "Cannot read properties of null (reading 'id')" appears occasionally, suggesting timing or reference issues

relacLine code

function num(v){ if(v==null) return 0; return Number(String(v).replace(',', '.')) || 0; }
function r2(n){ return Math.round((n + Number.EPSILON) * 100) / 100; }

const i = (typeof index !== 'undefined') ? Number(index) : Number(calcIndex.value ?? 0);
const f = (typeof field !== 'undefined') ? String(field) : String(calcField.value ?? 'net');
const rows = _.cloneDeep(ListViewData.value || []);
const row = rows[i] || {};

const qty = Math.max(0, num(row.ilosc));
const vat = Math.max(0, num(row.vat));
const k = 1 + vat/100;

let unitNet = num(row.cenaNetto);
let totalGross = num(row.wartoscBrutto);

if (f === 'net') {
  const unitGross = r2(unitNet * k);
  const totalNet = r2(qty * unitNet);
  const totalBrutto = r2(qty * unitGross);
  
  row.cenaNetto = unitNet;
  row.wartoscNetto = totalNet;
  row.wartoscBrutto = totalBrutto;
  row.source = 'net';
  
} else if (f === 'grossTotal') {
  const unitGross = qty > 0 ? totalGross / qty : 0;
  const calculatedUnitNet = k > 0 ? r2(unitGross / k) : 0;
  const totalNet = r2(qty * calculatedUnitNet);
  
  row.cenaNetto = calculatedUnitNet;
  row.wartoscNetto = totalNet;
  row.wartoscBrutto = r2(totalGross);
  row.source = 'grossTotal';
}


rows[i] = row;


ListViewData.setValue(rows);


setTimeout(() => {
  listView1.setData(rows);
}, 10);

listviewdata

[{
  id: 1,
  nazwa: '',
  ilosc: 0,
  jednostka: 'szt',
  cenaNetto: 0,
  vat: 8,
  wartoscNetto: 0,
  wartoscBrutto: 0,
  source: "net"
}]

additem

const currentData = ListViewData.value || [];
const newRow = {
    id: currentData.length + 1,
    nazwa: '',
    ilosc: 0,
    jednostka: 'szt',
    cenaNetto: 0,
    vat: 23, // To będzie wartość domyślna, ale powinna być możliwa do zaktualizowania
    wartoscNetto: 0,
    wartoscBrutto: 0
};
const newData = [...currentData, newRow]; // Prostsza składnia spread
ListViewData.setValue(newData);

deleteitem

// removeItem query - usuwanie po id
ListViewData.setValue(ListViewData.value.filter(row => row.id !== id));

calcindex

calcfield

defaultvalue and event handlers







VIDEO PRESENTING PROBLEM:

Thanks for providing such a detailed explanation, @Grzegorz_Piotrowski! It might have been difficult to understand, otherwise.

While it's possible that the ListView component itself is just being flaky, I agree that the likely culprit is the timing of all your onChange event handlers. One of the problems with defining so many handlers on a single component is that there is no way to enforce execution order. I usually get around this by coordinating them all in a dedicated JS query and then adding that as the sole event handler. In your case, it might look something like:

// coordinateEventHandlers
await ListViewData.setIn(...)
await ListViewData.setIn(...)
await calcIndex.setValue(...)
await calcField.setValue(...)
await recalcLine.trigger(...)
listView1.resetInstanceValues()

Doing it this way ensures execution order and, in this case, saves you the effort of defining and maintaining six different event handlers on each input. Let me know if refactoring your approach this way makes a difference!

1 Like

Thank you so much, Darren, for your help!
It turned out the issue was caused by having too many event handlers on the ListView fields, which created problems with properly refreshing the data.
I created a single script and added the necessary “Additional scope” values, which allowed me to reference them correctly in the query triggers.


1 Like