immerjs / use-immer

Use immer to drive state with a React hooks
MIT License
4.04k stars 92 forks source link

[BUG] Immer Error when setting properties on a Record object #113

Closed lautiab closed 1 year ago

lautiab commented 1 year ago

Description

When using the use-immer package with a reducer, I encountered a runtime error when trying to set a property on a Record<string, SomeType> object within the reducer function. The error message was:

[Immer] Immer only supports setting array indices and the 'length' property.

The exampleRecord object in my state is a Record<string, CustomType> and not an array, but it seems Immer might be interpreting the object as an array during its internal checks.

Code

Here is a simplified version of my reducer code:

export interface CustomType { /* ... */ }

export interface RecordData {
    id: string;
    label: string;
    exampleRecord: Record<string, CustomType>;
}

export interface ExampleState {
    tabData: RecordData[];
    /* ... */
}

const exampleReducer: ImmerReducer<ExampleState, ExampleAction> = (state, action) => {
    switch (action.type) {
        case ExampleActionTypes.ADD_PROPERTY: {
            const uniqueId = `id--${uuidv4()}`;

            state.tabData[state.selectedTab].exampleRecord[uniqueId] = {
                /* ... */
            };
            break;
        }
        /* ... */
    }
};

// In the component:
const [state, dispatch] = useImmerReducer(exampleReducer, initialState);

Workaround

Just in case it works for anybody facing the same, or to find the issue faster..

Creating a new object with the updated exampleRecord and then assigning it back to the draft state:

case ExampleActionTypes.ADD_PROPERTY: {
    const uniqueId = `id--${uuidv4()}`;
    const newObject: CustomType = { /* ... */ };

    state.tabData[state.selectedTab].exampleRecord = {
        ...state.tabData[state.selectedTab].exampleRecord,
        [uniqueId]: newObject,
    };
    break;
}

This workaround avoids the error, but it would be great if Immer could handle setting properties on Record objects without the need for this workaround.

mweststrate commented 1 year ago

Immer doesn't see arrays where they aren't. One of the index accessors in your code might actually be a string instead of a number. Pretty sure you're making a mistake somewhere in there, but hard to tell without a repro.

On Thu, 6 Apr 2023, 23:17 lautiAB, @.***> wrote:

Description

When using the use-immer package with a reducer, I encountered a runtime error when trying to set a property on a Record<string, SomeType> object within the reducer function. The error message was:

[Immer] Immer only supports setting array indices and the 'length' property.

The exampleRecord object in my state is a Record<string, CustomType> and not an array, but it seems Immer might be interpreting the object as an array during its internal checks. Code

Here is a simplified version of my reducer code:

export interface CustomType { / ... / } export interface RecordData { id: string; label: string; exampleRecord: Record<string, CustomType>;} export interface ExampleState { tabData: RecordData[]; / ... /} const exampleReducer: ImmerReducer<ExampleState, ExampleAction> = (state, action) => { switch (action.type) { case ExampleActionTypes.ADD_PROPERTY: { const uniqueId = id--${uuidv4()};

        state.tabData[state.selectedTab].exampleRecord[uniqueId] = {
            /* ... */
        };
        break;
    }
    /* ... */
}};

// In the component:const [state, dispatch] = useImmerReducer(exampleReducer, initialState);

Workaround

Just in case it works for anybody facing the same, or to find the issue faster..

Creating a new object with the updated exampleRecord and then assigning it back to the draft state:

case ExampleActionTypes.ADD_PROPERTY: { const uniqueId = id--${uuidv4()}; const newObject: CustomType = { / ... / };

state.tabData[state.selectedTab].exampleRecord = {
    ...state.tabData[state.selectedTab].exampleRecord,
    [uniqueId]: newObject,
};
break;}

This workaround avoids the error, but it would be great if Immer could handle setting properties on Record objects without the need for this workaround.

— Reply to this email directly, view it on GitHub https://github.com/immerjs/use-immer/issues/113, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBCKJBSUHWUVKBQIPDTW74XHFANCNFSM6AAAAAAWV4TPSY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

mweststrate commented 1 year ago

E.g. state.selectTab is probably the issue, not the uniqueId?

On Thu, 6 Apr 2023, 23:24 Michel Weststrate, @.***> wrote:

Immer doesn't see arrays where they aren't. One of the index accessors in your code might actually be a string instead of a number. Pretty sure you're making a mistake somewhere in there, but hard to tell without a repro.

On Thu, 6 Apr 2023, 23:17 lautiAB, @.***> wrote:

Description

When using the use-immer package with a reducer, I encountered a runtime error when trying to set a property on a Record<string, SomeType> object within the reducer function. The error message was:

[Immer] Immer only supports setting array indices and the 'length' property.

The exampleRecord object in my state is a Record<string, CustomType> and not an array, but it seems Immer might be interpreting the object as an array during its internal checks. Code

Here is a simplified version of my reducer code:

export interface CustomType { / ... / } export interface RecordData { id: string; label: string; exampleRecord: Record<string, CustomType>;} export interface ExampleState { tabData: RecordData[]; / ... /} const exampleReducer: ImmerReducer<ExampleState, ExampleAction> = (state, action) => { switch (action.type) { case ExampleActionTypes.ADD_PROPERTY: { const uniqueId = id--${uuidv4()};

        state.tabData[state.selectedTab].exampleRecord[uniqueId] = {
            /* ... */
        };
        break;
    }
    /* ... */
}};

// In the component:const [state, dispatch] = useImmerReducer(exampleReducer, initialState);

Workaround

Just in case it works for anybody facing the same, or to find the issue faster..

Creating a new object with the updated exampleRecord and then assigning it back to the draft state:

case ExampleActionTypes.ADD_PROPERTY: { const uniqueId = id--${uuidv4()}; const newObject: CustomType = { / ... / };

state.tabData[state.selectedTab].exampleRecord = {
    ...state.tabData[state.selectedTab].exampleRecord,
    [uniqueId]: newObject,
};
break;}

This workaround avoids the error, but it would be great if Immer could handle setting properties on Record objects without the need for this workaround.

— Reply to this email directly, view it on GitHub https://github.com/immerjs/use-immer/issues/113, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBCKJBSUHWUVKBQIPDTW74XHFANCNFSM6AAAAAAWV4TPSY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

lautiab commented 1 year ago

I believe the error is not due to using a string index accessor with an array, but rather due to limitations of Immer when setting properties on Record objects directly.

For your information, selectedTab is of the number type, and the provided workaround wouldn't have worked if the issue was coming from there.

I'm using: "immer": "^9.0.21", "use-immer": "^0.8.1", "typescript": "^4.9.3",

mweststrate commented 1 year ago

Record is not an object, it's a type, it doesn't exists at runtime, so it doesn't relate to what immer can and cannot handle. So it seems that there is something not correct in your application, but you'll need to repro otherwise one can't help with that.

On Fri, 7 Apr 2023, 00:11 lautiAB, @.***> wrote:

I believe the error is not due to using a string index accessor with an array, but rather due to limitations of Immer when setting properties on Record objects directly.

For your information, selectedTab is of the number type, and the provided workaround wouldn't have worked if the issue was coming from there.

I'm using: "immer": "^9.0.21", "use-immer": "^0.8.1", "typescript": "^4.9.3",

— Reply to this email directly, view it on GitHub https://github.com/immerjs/use-immer/issues/113#issuecomment-1499686956, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBFFKZJN45UC2ORIBSLW745Q3ANCNFSM6AAAAAAWV4TPSY . You are receiving this because you commented.Message ID: @.***>

lautiab commented 1 year ago

Hey again, I've been digging around and found that at initialization I was still using an array instead of an object. I believe TS was not failing at buildtime, cause I've messed up with the location of the Interface. Now it's working correctly, thanks for your help though!