Error can't define property "__gohashid": Object is not extensible

Hi, Walter.

I faced with problem and not know how truly describe it.

I`m using react with ****.

NodeData Interface

key: number
category: 'Source'
header: {
	text: string
	styles: {
	  isBold: boolean
	  isCursive: boolean
	  isUnderLine: boolean
	  fontFamily: string
	  fontSize: number
	  fontColor: string
	  strokeColor: string
	  strokeWidth: number
	  isDotted: boolean
	  backgroundColor: string
	  width: number
	  height: number
	 }
	}
settings: {
  headerTextSize: string
  bodyTextSize: string
}
adornmentList: Array<{
  id: string
  category: tAdornmentCategory,
  styles: {
	  isBold: boolean
	  isCursive: boolean
	  isUnderLine: boolean
	  fontFamily: string
	  fontSize: number
	  fontColor: string
	  strokeColor: string
	  strokeWidth: number
	  isDotted: boolean
	  backgroundColor: string
	  width: number
	  height: number
	  alignment: string
  },
  value: string
}>

nodeTemplate

const BodyShapeSettings: Partial<go.Shape> = {
  fill: 'white',
  opacity: 1,
  stroke: 'blue',
  strokeWidth: 2,
  strokeDashArray: [8, 4],
};

const BodyTextBlockSettings: Partial<go.TextBlock> = {
  // editable: true,
  cursor: 'move',
  isMultiline: true,
  overflow: go.TextBlock.OverflowEllipsis,
  textAlign: 'center',
  name: TEXT_BLOCK_BODY_NAME,
  width: 100,
  click: shapeClick,
};

const BodyPanelSettings: Partial<go.Panel> = {
  // alignment: new go.Spot(0, 0, 0, 0),
  margin: 0,
  padding: 0,
};

const makeSourceBodyShape = () => new go.Shape('RoundedRectangle', BodyShapeSettings);
const makeSourceBody = () => new go.TextBlock('body_text', BodyTextBlockSettings)
  .bind(makeTwoWayBinding('desiredSize', 'bodyTextSize', 'settings', go.Size.parse, go.Size.stringify));

const makeSourceBodyPanel = () => new go.Panel(go.Panel.Auto, BodyPanelSettings).add(makeSourceBodyShape()).add(makeSourceBody());

const HeaderShapeSettings: Partial<go.Shape> = {
};

const HeaderTextBlockSettings: Partial<go.TextBlock> = {
  // editable: true,
  cursor: 'move',
  isMultiline: true,
  overflow: go.TextBlock.OverflowEllipsis,
  textAlign: 'center',
  name: TEXT_BLOCK_HEADER_NAME,
  width: 100,
  click: shapeClick,
};

const HeaderPanelSettings: Partial<go.Panel> = {
  margin: 0,
  padding: 0,
};

const makeSourceHeaderShape = () => new go.Shape('RoundedRectangle', HeaderShapeSettings)
  .bind(new go.Binding('stroke', 'header', (value: iSourceHeaderNodeData) => value.styles.strokeColor))
  .bind(new go.Binding('strokeWidth', 'header', (value: iSourceHeaderNodeData) => value.styles.strokeWidth))
  .bind(new go.Binding('fill', 'header', (value: iSourceHeaderNodeData) => value.styles.backgroundColor));

const makeSourceHeaderBody = () => new go.TextBlock(HeaderTextBlockSettings)
  .bind(new go.Binding('stroke', 'header', (value: iSourceHeaderNodeData) => value.styles.fontColor))
  .bind(new go.Binding('isUnderline', 'header', (value: iSourceHeaderNodeData) => value.styles.isUnderLine))
  .bind(new go.Binding('text', 'header', (value: iSourceHeaderNodeData) => value.text))
  .bind(new go.Binding('font', 'header', (value: iSourceHeaderNodeData) => renderFont(value.styles)))
  .bind(makeTwoWayBinding('desiredSize', 'headerTextSize', 'settings', go.Size.parse, go.Size.stringify));

const makeSourceHeaderPanel = () => new go.Panel(go.Panel.Auto, HeaderTextBlockSettings)
  .add(makeSourceHeaderShape())
  .add(makeSourceHeaderBody());


const createSourceTemplate = () => $(
  go.Node,
  go.Panel.Vertical,
  {
    selectionObjectName: MAIN_SHAPE_NAME,
    resizable: true,
    resizeObjectName: TEXT_BLOCK_HEADER_NAME,
    locationSpot: go.Spot.Center,
  },
  new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
  $(
    go.Panel,
    go.Panel.Vertical,
    makeSourceHeaderPanel(),
    makeSourceBodyPanel(),
  ),
);

makeTwoWayBinding

const makeTwoWayBinding = (
  targetName: string,
  sourceName: string,
  objectName: string,
  conversationFn?: (value: string, target: string) => void,
  backConversationFn?: typeof go.Spot.stringify | typeof go.Size.stringify | typeof go.Point.stringify | tBackConversationFn,
  isUpdateNode?: boolean,
) => {
  const bind = new go.Binding(targetName, objectName);

  bind.mode = go.Binding.TwoWay;
  bind.converter = (diagramSettings, target) => {
    const value = diagramSettings[sourceName];
    if (value === undefined) return target[targetName];
    return (typeof conversationFn === 'function') ? conversationFn(value, target) : value;
  };

  bind.backConverter = (value, data, model) => {
    const objectData = data[objectName];
    if (model) {
      model.setDataProperty(objectData, sourceName, (typeof backConversationFn === 'function')
        ? backConversationFn(value, objectData, model)
        : value);
      if (isUpdateNode) {
        model.updateTargetBindings(data);
      }
    } else {
      objectData[sourceName] = (typeof backConversationFn === 'function' ? backConversationFn(value, objectData, model) : value);
    }
    return objectData;
  };

  return bind;
};

My Reducers

const setNodeDataArray = (state: iDiagramState, action: PayloadAction<{ nodeDataArray: tDiagramObjectData[], isSkip?: boolean }>) => {
    state.nodeDataArray = action.payload.nodeDataArray;
    state.isSkipsDiagramUpdate = action.payload.isSkip || false;
  };
const changeNodeStyle = (state: iDiagramState) => {
    state.nodeDataArray.forEach((item) => {
      item.header.styles.backgroundColor = 'brown';
      return item;
    });
    state.isSkipsDiagramUpdate = false;
  };

The changeNodeStyle change backgroundColor property in any nodes;

After dispatch changeNodeStyle backgroundColor successfully updates. But then when I try resize body or header panel I get next error.
If I change size body or header panel without update backgroundColor done without error.

If I change node location, the error not appear.
How I can solve it?

Error appear when I change properties in internal Objects such as header, body or settings

I read any examples from gojs docs.

In addition, attach code Diagram and modelUpdate function

const initSourceDiagram = () => {
  const $ = go.GraphObject.make;
  const diagram = $(go.Diagram, {
    'undoManager.isEnabled': true, // must be set to allow for model change listening
    model: $(go.GraphLinksModel, {
      linkKeyProperty: 'key',
      linkFromKeyProperty: 'from',
      linkToKeyProperty: 'to',
      copyNodeDataFunction: (original: go.ObjectData) => { // using only copy/past func
        const newdata = { ...original };
        return newdata;
      },
    }),
  });

  diagram.nodeTemplateMap.add(NodeCategories.Source, createSourceTemplate());
  const getDiagram = () => diagram;

  return {
    getDiagram,
  };
};
function handleModelChange(obj: go.IncrementalData) {
    if (obj === null) return;
    const { insertedNodeKeys } = obj;
    const { modifiedNodeData } = obj;
    const { removedNodeKeys } = obj;

    // copy data to new array, but maintain references
    let nodeArr = nodeDataArray.slice() as go.ObjectData[];

    const modifiedNodeMap = new Map();

    let arrChanged = false;

    // handle node changes
    if (modifiedNodeData) {
      modifiedNodeData.forEach((nd) => {
        modifiedNodeMap.set(nd.key, nd);
        const idx = mapNodeKeyIdx.get(nd.key);
        if (idx !== undefined && idx >= 0) {
          nodeArr.splice(idx, 1, nd);
          arrChanged = true;
        }
      });
    }
    if (insertedNodeKeys) {
      insertedNodeKeys.forEach((key) => {
        const nd = modifiedNodeMap.get(key);
        const idx = mapNodeKeyIdx.get(key);
        if (nd && idx === undefined) {
          mapNodeKeyIdx.set(nd.key, nodeArr.length);
          nodeArr.push(nd);
          arrChanged = true;
        }
      });
    }
    if (removedNodeKeys) {
      nodeArr = nodeArr.filter((nd) => {
        if (removedNodeKeys.includes(nd.key)) {
          arrChanged = true;
          return false;
        }
        return true;
      });
      refreshNodeIndex(nodeArr);
    }

    if (arrChanged) {
      updateDataModel({ nodeDataArray: nodeArr as typeof nodeDataArray, isSkip: true });
    }
  }

So it sounds like updates to data that’s nested in your node data array may be what’s causing problems. Are you sure you’re immutably updating that data? You need to make sure that if you change any data property, you are creating a new object reference.

I get same error if I change size body or header panel on the first node and then when I try change size on other node

In this case I change store from diagram. May error stay in makeTwoWayBinding method?

I’m suggesting that you need to change your handleModelChange function. It looks like it’s keeping the same data object references instead of immutably updating them.

Notice how the gojs-react-basic sample uses the immer library to make immutable data updates. https://github.com/NorthwoodsSoftware/gojs-react-basic/blob/master/src/App.tsx#L141

handleModelChange implements according to store library. I`, using redux/toolkit. And according it I send new (not mutating) Object

const setNodeDataArray = (state: iDiagramState, action: PayloadAction<{ nodeDataArray: tDiagramObjectData[], isSkip?: boolean }>) => {
    state.nodeDataArray = action.payload.nodeDataArray;
    state.isSkipsDiagramUpdate = action.payload.isSkip || false;
  };

let nodeArr = nodeDataArray.slice()

Immer not resolve problem

Whatever is triggering your React state updates is not making immutable changes, which is why you’re seeing that error. We don’t know about Redux Toolkit. I’m not sure how much we can help you with so many other libraries involved.

On the gojs-react side, we ensure that objects passed into the diagram model are deep cloned so that any mutation done by GoJS is safe.

Redux Toolkit’s createReducer and createSlice automatically use Immer internally to let you write simpler immutable update logic using “mutating” syntax. This helps simplify most reducer implementations.

Why property __gohashid not presents in state? I think problem with it…
When __gohashid is losted in diagram model, errror is appear((
This happens when there are several objects on the diagram.

I noticed that the __gohashid property changes after parameter headerTextSize or bodyTextSize changed. Object settings has own __gohashid

It should be present in the Node data (GoJS adds it). I have no idea why it would be getting removed, unless something you are doing is modifying or replacing the data.

__gohashid should be on the GoJS model data but not on the React state.

I try using handleModelChange from example but get the same error when I try to change header size. I think problem with in function makeTwoWayBinding

Is your header text resizable? You may just want a one way binding there anyways.

Maybe you can put breakpoints in the conversion functions to see if they are causing problems.

Yes, headerText is resizable. What does mean can’t define property “__gohashid”: Object is not extensible error? I dont now what I should find

That’s not a GoJS error. That’s an error thrown by React when you try to mutate data. Your converter functions are probably mutating data and not going through Redux toolkit to make the changes immutable. You may need to write them to be immutable.

How to properly change a property of a nested object in binding function?
Such as settings

{
key:number,
settings: {
    headerTextSize: string
    bodyTextSize: string
  }
}

So what is the data object before and after the conversion takes place? It’s still hard to tell what’s causing the error, and it is certainly something outside GoJS. And are you sure your state object and GoJS model data are not sharing references?

I’m guessing there’s a bug in the method I use to convert variables headerTextSize bodyTextSize when resizing node components.
I assumed this because the loc is at the top level and therefore there are no external references left after the conversion. I tried using deep copy for objects, but it didn’t work. This is why I came to the assumption that I need to look for an error in the methods. Below I will duplicate my data model for the node and the data conversion function for the variables headerTextSize bodyTextSize

key: number
category: 'Source'
header: {
	text: string
	styles: {
	  isBold: boolean
	  isCursive: boolean
	  isUnderLine: boolean
	  fontFamily: string
	  fontSize: number
	  fontColor: string
	  strokeColor: string
	  strokeWidth: number
	  isDotted: boolean
	  backgroundColor: string
	  width: number
	  height: number
	 }
	}
settings: {
  headerTextSize: string
  bodyTextSize: string
}
adornmentList: Array<{
  id: string
  category: tAdornmentCategory,
  styles: {
	  isBold: boolean
	  isCursive: boolean
	  isUnderLine: boolean
	  fontFamily: string
	  fontSize: number
	  fontColor: string
	  strokeColor: string
	  strokeWidth: number
	  isDotted: boolean
	  backgroundColor: string
	  width: number
	  height: number
	  alignment: string
  },
  value: string
}>
}
const makeSourceHeaderBody = () => new go.TextBlock(HeaderTextBlockSettings)
 .bind(new go.Binding('stroke', 'header', (value: iSourceHeaderNodeData) => value.styles.fontColor))
 .bind(new go.Binding('isUnderline', 'header', (value: iSourceHeaderNodeData) => value.styles.isUnderLine))
 .bind(new go.Binding('text', 'header', (value: iSourceHeaderNodeData) => value.text))
 .bind(new go.Binding('font', 'header', (value: iSourceHeaderNodeData) => renderFont(value.styles)))
 .bind(makeTwoWayBinding('desiredSize', 'headerTextSize', 'settings', go.Size.parse, go.Size.stringify));
const createSourceTemplate = () => $(
  go.Node,
  go.Panel.Vertical,
  {
    selectionObjectName: MAIN_SHAPE_NAME,
    resizable: true,
    resizeObjectName: TEXT_BLOCK_HEADER_NAME,
    locationSpot: go.Spot.Center,
  },
  new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
  $(
    go.Panel,
    go.Panel.Vertical,
    makeSourceHeaderPanel(),
    makeSourceBodyPanel(),
  ),
);
const makeTwoWayBinding = (
  targetName: string,
  sourceName: string,
  objectName: string,
  conversationFn?: (value: string, target: string) => void,
  backConversationFn?: typeof go.Spot.stringify | typeof go.Size.stringify | typeof go.Point.stringify | tBackConversationFn,
  isUpdateNode?: boolean,
) => {
  const bind = new go.Binding(targetName, objectName);

  bind.mode = go.Binding.TwoWay;
  bind.converter = (diagramSettings, target) => {
    const value = diagramSettings[sourceName];
    if (value === undefined) return target[targetName];
    return (typeof conversationFn === 'function') ? conversationFn(value, target) : value;
  };

  bind.backConverter = (value, data, model) => {
    const objectData = data[objectName];
    if (model) {
      model.setDataProperty(objectData, sourceName, (typeof backConversationFn === 'function')
        ? backConversationFn(value, objectData, model)
        : value);
      if (isUpdateNode) {
        model.updateTargetBindings(data);
      }
    } else {
      objectData[sourceName] = (typeof backConversationFn === 'function' ? backConversationFn(value, objectData, model) : value);
    }
    return objectData;
  };

  return bind;
};

If we consider the operation of the function makeTwoWayBinding step by step, then after updating the model the callback converter works twice, the first time with new data, and the second time with old data.

We’re looking into this more.

It turns out immer automatically freezes any objects it produces. Auto freezing | Immer

When you update a nested property, GoJS doesn’t make a deep clone of the containing object, so you end up with a frozen object in the GoJS model, which expects to be mutable. We’ll consider whether it’s feasible for us to change this.

In the meantime, immer provides a way to stop the freezing of produced objects by calling setAutoFreeze(false). This should resolve your issue.

Example:

Thank you very much for your detailed answer. Then the question arises why Immer does not freeze the go.ObjectData, but only the objects nested in it. After all, changing the loc variable works fine. And even changing variables

settings: {
  headerTextSize: string
  bodyTextSize: string
}

works fine, but only if there is only one node on the diagram. When the second node appears, changing the nested objects in go.ObjectData previouse Nodes results in the above error.