Skip to main content

Anti-Patterns and Known Issues

This page documents common mistakes found in real Protrak custom widget implementations, ordered from most to least frequently seen.


1. Function Name Mismatch with Widget Title

Symptom: Widget renders "Custom widget function not found" error on the page.

Cause: The JavaScript function name does not exactly match the Widget Title registered in the platform.

Fix: The function name must be an exact case-sensitive match of the Widget Title:

// Widget Title in admin: "InvoiceLineItemsEditLayout"
function InvoiceLineItemsEditLayout(pageContext) { ... } // ✅ matches

// Widget Title in admin: "InvoiceLineItemsEditLayout"
function invoiceLineItemsEditLayout(pageContext) { ... } // ❌ wrong case

2. requestConfig Not Wrapped in useCallback

Symptom: Widget enters an infinite API call loop — each render creates a new function reference, which useProtrakApi interprets as a new request.

Bad:

function MyWidget(pageContext) {
// New function reference on every render → infinite loop
const getConfig = ({ instanceId }) => ({
endpoint: `instances/${instanceId}`,
config: { method: 'GET' },
});

const { state } = useProtrakApi({
requestConfig: getConfig,
instanceId: pageContext.instanceId,
});
}

Fix: Wrap in React.useCallback:

const getConfig = React.useCallback(
({ instanceId }) => ({
endpoint: `instances/${instanceId}`,
config: { method: 'GET', params: { 'attributes[0]': 'Name' } },
}),
[] // Include any closure variables used inside
);

3. Hooks Called Inside Conditionals

Symptom: React error: "Hooks can only be called inside the body of a function component."

Bad:

function MyWidget(pageContext) {
const { editedValues } = pageContext;

// WRONG: hooks cannot be called conditionally
if (editedValues?.SomeAttr?.arrayValue[0] === 'SpecialType') {
const { state } = useProtrakApi({ requestConfig: myConfig });
}
}

Fix: Always call hooks unconditionally at the top level. Use the result conditionally:

function MyWidget(pageContext) {
// CORRECT: hook called unconditionally
const { state } = useProtrakApi({ requestConfig: myConfig });

const isSpecialType =
pageContext.editedValues?.SomeAttr?.arrayValue?.[0] === 'SpecialType';
if (!isSpecialType) return null;
// ...
}

4. Module-Level Variables Causing Global Scope Pollution

Symptom: Naming conflict between two widgets; one widget's filter state bleeds into another widget rendered on the same page.

Bad:

// These are at the top of the file, OUTSIDE the widget function
let elementTypes = new Set();
let levels = new Set();
let grandTotal = {};

function AnyWidget(pageContext) { ... }

Fix: Move all variables inside the widget function. Use React.useRef or React.useMemo for values that need to persist across renders without causing re-renders:

function AnyWidget(pageContext) {
// CORRECT: scoped to the component
const grandTotal = React.useRef({});
const [elementTypes, setElementTypes] = React.useState(new Set());
// ...
}

Platform constraint reminder: Any variable or function defined outside the widget function is exposed to window and may conflict with other widgets loaded on the same page.


5. Using One Widget for Both Edit and View Modes

Symptom: On view mode, the widget renders editable inputs. On edit mode, it may have stale read-only display.

Recommendation: Create separate widgets for edit and view:

  • MyDataEditLayout.js — edit mode with onAttributeEdit, add/delete rows
  • MyDataViewLayout.js — view mode reading from attributeValues, read-only rendering
// View-only variant — no editing controls
function InvoiceLineItemsEditorViewLayout(pageContext) {
const { attributeValues } = pageContext;
const rows = JSON.parse(
attributeValues['InvoiceIineItemData']?.textValue || '[]'
);

return (
<table>
{rows.map((row, i) => (
<tr key={i}>
<td>{row.Description}</td>
<td>{row['No. of resources']}</td>
{/* No TextBox, no delete button, no onAttributeEdit */}
</tr>
))}
</table>
);
}

6. Leftover debugger Statements

Symptom: Widget halts execution when DevTools is open; users see a blank widget in the browser with DevTools active.

Bad:

debugger;
function CSADocumentReport(pageContext) { ... }

Fix: Remove all debugger statements before deploying.


7. Undefined Variable Passed to Sub-Component

Symptom: A wrapped editor component (e.g. SurveyCreatorWidget) opens blank — previously saved data is lost.

Bad:

// surveyJSONValue is never defined — it is undefined
<SurveyCreatorWidget
surveyJSONValue={surveyJSONValue}
onEdit={onAttributeEdit}
/>

Fix: Always read the initial value from attributeValues:

const surveyJSONValue = attributeValues['SurveyJSON']?.textValue ?? '';
<SurveyCreatorWidget
surveyJSONValue={surveyJSONValue}
onEdit={onAttributeEdit}
/>;

8. Missing canUpdate: true in onAttributeEdit Payload

Symptom: Widget appears to work — no errors — but the attribute value is silently dropped from the save payload.

Bad:

onAttributeEdit(
'MyAttribute',
{ name: 'MyAttribute', type: 'Text', textValue: JSON.stringify(data) },
''
);

Fix: Always include canUpdate: true:

onAttributeEdit(
'MyAttribute',
{
name: 'MyAttribute',
type: 'Text',
canUpdate: true,
textValue: JSON.stringify(data),
},
''
);

9. Missing Third Argument in onAttributeEdit

Symptom: Runtime error or attribute change not registered.

Bad:

onAttributeEdit('MyAttribute', attrObject);

Fix: Always pass an empty string as the third argument:

onAttributeEdit('MyAttribute', attrObject, '');

10. Not Restoring Saved State from attributeValues on Mount

Symptom: Widget shows empty table/form when user opens an instance that already has saved data. Previous data appears lost until re-entered.

Bad:

const [rows, setRows] = React.useState([]); // Always starts empty

Fix: Load from attributeValues in a useEffect on mount:

const [rows, setRows] = React.useState([]);

React.useEffect(() => {
const savedJson = attributeValues['MyAttribute']?.textValue;
if (savedJson) {
try {
const parsed = JSON.parse(savedJson);
if (Array.isArray(parsed)) setRows(parsed);
} catch {
// Ignore malformed data
}
}
}, [attributeValues]);