React Hook Errors with Material UI inside Forge Confluence Custom UI

Hello,

I’m having an issue with Forge Custom UI for Confluence and integrating Material UI’s XGrid/DataGridPro product which seems to be related to the MaterialUI product itself but it works independent of Confluence so I’m not sure what the problem is.

I’m getting the following error:

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

react-dom.production.min.js:209 Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at P (react.production.min.js:18:327)
at Object.t.useRef (react.production.min.js:23:226)
at index-esm.js:16:118071
at index-esm.js:16:171492
at Za (react-dom.production.min.js:153:146)
at Bi (react-dom.production.min.js:173:192)
at Eu (react-dom.production.min.js:265:425)
at bl (react-dom.production.min.js:246:265)
at ml (react-dom.production.min.js:246:194)
at ul (react-dom.production.min.js:239:172)

I’ve tried the following to no avail:

  1. Adding unsafe-line to the manifest.yml file.
  2. Using a variety of different structures for where to place the hook calls and rendering.
  3. Using inline demo data instead of actually pulling data via fetch.
  4. Using both Material UI 4 (XGrid) and MUI 5 (DataGridPro) versions.

And I’m still ending up with the same result. Any advice would be greatly appreciated. Code reference is included below. Thank you!

My server-side index.js

resolver.define('getTable', async (req) =>
{
    const response = await fetch("https://url-here",
        {
            headers:
                {
                    "Accept": "application/vnd.github.v3.raw", // only get raw file from request
                    "Authorization": "token " + process.env.GITHUB_KEY
                }
        });
    return response.text();
});

My client-side App.js

function App() {
    const [data, setData] = useState(null);
    useEffect( () => {
        invoke('getTable', {example: 'my-invoke-variable'}).then(setData);
    }, []);
    return (
        <Fragment>
            {getData(data)}
        </Fragment>
    )
}
export function getData(data) {
    if(!data)
    {
        return <div>Loading...</div>;
    }
    const lines = data.split('\n');
    const headers = lines[0].split('\t');
    const types = lines[1].split('\t');
    const columnsz = [];
    const rowsz = [];
    // map each cell in a row of data to the corresponding column key then insert
    for (var i = 2; i < lines.length; i++)
    {
        var row = {};
        const line = lines[i].split('\t');
        for (var c = 0; c < line.length; c++)
        {
            row["id"] = i+1;
            if(line[c]) {
                row[headers[c]] = line[c];
            }
                // for empty cells, we'll populate with the column's default value if we have one
            // (skip columns with required or include constraints since those aren't default values)
            else
            {
                if(types[c].includes('[') &&
                    !types[c].toLowerCase().includes('required') &&
                    !types[c].toLowerCase().includes('unique')
                )
                {
                    row[headers[c]] = types[c].split('[').pop().split(']')[0];
                }
                else // if we didn't find a default value specified, fall back to logic in DataTableColumnType.cpp
                {
                    switch(types[c].charAt(0))
                    {
                        case "i":
                        case "f":
                        case "b":
                            row[headers[c]] = "0";
                            break;
                        default:
                            row[headers[c]] = "";
                            break;
                    }
                }
            }
        }
        rowsz.push(row);
    }
    // populate the header data for each column
    for (var i = 0; i < headers.length; i++)
    {
        columnsz.push({
            field: headers[i],
            description: types[i],
            editable: false,
            resizable: true,
            sortable: true,
            hideSortIcons: true,
            headerClassName: 'dt-xgrid-col-header',
            align: 'left',
            width: 150,
            type: getColumnType(types[i]),
            valueOptions: getEnumValues(types[i])
        })
    }
    return (
            <XGrid
                columns={columnsz}
                rows={rowsz}
                components={{
                         Toolbar: DataTableUpperToolbar,
                         ColumnMenu: DataTableColumnMenu,
                }}
                showCellRightBorder
                showColumnRightBorder
                pagination
                density="compact"
                disableColumnReorder
                disableDensitySelector
                onColumnHeaderClick={(params: GridColumnHeaderParams, event: MuiEvent<React.SyntheticEvent>, details?: any) => {
                        event.defaultMuiPrevented = true;
                }}
            />
    );
}