eslint-plugin-pinia-getters-no-array-mutations

1.2.0 • Public • Published

eslint-plugin-pinia-getters-no-array-mutations

This ESLint plugin introduces a rule for Pinia, the intuitive, type-safe, and flexible store pattern for Vue.js applications. By ensuring that getters in Pinia stores avoid in-place array mutations on the store's state, it prevents side effects, hard-to-track reactivity issues and unexpected infinite recursive behavior, ensuring a smoother development experience and less frustrating bug hunts. :)

Installation

If you haven't installed eslint in your project yet, follow its installation guide.

Then, install this plugin:

npm install eslint-plugin-pinia-getters-no-array-mutations --save-dev

Usage

To include the rule in your ESLint checks, add it to your .eslintrc configuration like this:

{
  "plugins": [
    // ... other plugins
    "pinia-getters-no-array-mutations"
  ],
  "rules": {
    // ... other rules
    "pinia-getters-no-array-mutations/no-array-mutations-on-state": "error",
  }
}

Fix

The rule offers an automatic fix in the form of creating a clone of the original structure via structuredClone. Be aware that this changes the semantics of your code, therefore it is disabled by default. If you are not sure what structuredClone does or if it's the right approach for you, please read the section Understanding the Fix.

You can enable the automatic fix in you .eslintrc configuration like this:

"rules": {
  // ...
  "pinia-getters-no-array-mutations/no-array-mutations-on-state": ["error", { "enableFix": true }]
  // ...
}

Executing the fix will wrap the affected property in a structuredClone() call:

// Before:
sortedDifferently: (state) => state.someArray.sort(sortFn)

// After:
sortedDifferently: (state) => structuredClone(state.someArray).sort(sortFn)

About the rule: no-array-mutations-on-state

This rule checks for and reports any usage of array methods that mutate the store's state directly within Pinia getters.

Examples

Invalid Code Examples

The rule will flag any use of mutating array methods that are directly applied to this, state, or any nested properties within a Pinia getter function. These can modify your state in place and lead to infinite recursive behavior. Here are some examples that will be flagged by the rule:

const store = {
  getters: {
    // Using .reverse() directly on this.someArray
    reversedArray: (state) => this.someArray.reverse(),

    // Mutating a nested array on this
    sortedNestedArray: (state) => this.nested.someArray.sort(sortFn),

    // Same idea for state
    sortedStateArray: (state) => state.someArray.sort(sortFn),

    // Mutating a nested array on state
    sortedStateNestedArray: (state) => state.nested.someArray.sort(sortFn),
  }
};

Valid Code Examples

The following code examples use non-mutating patterns and will not be flagged by the rule:

const store = {
  getters: {
    // Making a shallow copy of an array on this before sorting - may still be dangerous if you have nested arrays
    sortedArray: (state) => [...this.someArray].sort(sortFn),
    sortedNestedArray: (state) => [...this.nested.someArray].sort(sortFn),
    
    // Same for state
    sortedStateArray: (state) => [...state.someArray].sort(sortFn),
    sortedStateNestedArray: (state) => [...state.nested.someArray].sort(sortFn),
    
    // Using structuredClone to deeply clone an array on this before sorting
    sortedDeepCloneArray: (state) => structuredClone(this.someArray).sort(sortFn),
    sortedStateDeepCloneNestedArray: (state) => structuredClone(state.nested.someArray).sort(sortFn),
  }
};

What This Rule Checks

  • Any instance where this, state or any member like state.someArray this.deeply.nested.property is followed by a call to a mutating array method.
  • Usage of push, pop, shift, unshift, splice, sort, reverse, fill, and copyWithin on arrays that are part of the store's reactive state.

Limitations / What This Rule Doesn't Check

Mutating other stores

The rule currently does not cover cases where other stores' getters or properties are mutated. For example:

getters: {
  sortedDifferently: () => {
    const otherStore = useOtherStore();
    otherStore.someArray.sort(sortFn); // Not currently flagged by this rule, but still dangerous
  }
}

Since the rule does not flag the mutation of other properties, e.g. from other stores, you may still end up with methods that mutate arrays in place. This is a trade-off because the alternative would be to make the rule very broad, which could end up causing a lot of false positives, e.g. flagging operations on non-reactive arrays. If you have a good approach to improve this, please open a pull request or raise an issue :)

Complex reactivity inheritance

If you build complex getters, for example by using Maps, Sets, Object.values or similar to get a subset of your state, and then use methods that mutate this subset, it might affect the underlying reactive state. This rule is not made for complex use cases like that.

Your copy method

The rule does not check the contents of your array. If you create a shallow copy of a deeply nested array, you may still end up mutating properties of the original state if you mutate it. Example:

getters: {
  sortedDifferently: () => {
    const otherStore = useOtherStore();
    otherStore.someArray.sort(sortFn); // Not currently flagged by this rule, but still dangerous
  }
}

To avoid this, use a deep cloning method like structuredClone or any library of your choosing, popular choices being lodash's _.cloneDeep or klona.

Understanding the Automatic Fix: structuredClone Deep Cloning

This plugin offers an automatic fix to prevent array mutations by cloning the target array before applying methods like .sort(). The cloning is done using the structuredClone function, a recent addition to the JavaScript language that creates a deep clone of a given object, preserving the structure and data of the original without any reference to it.

Implications of Using structuredClone:

  • Deep Cloning: Unlike shallow copy techniques, structuredClone will recursively copy all properties, leading to a completely new array while maintaining nested data. This is useful for preventing side effects in reactive state management but be mindful of any performance implications.
  • Compatibility: structuredClone is supported in modern browsers and Node.js environments; however, if you are targeting older environments, a polyfill or alternative cloning method may be necessary. Check a compatibility table like caniuse for up-to-date support information.
  • Performance Considerations: Deep cloning an entire structure can be more resource-intensive than shallow cloning. While this shouldn't be an issue for small to medium-sized arrays, it's important to be cautious with large or complex state objects, as the operation could impact performance or memory usage.
  • Semantic Changes: Keep in mind that wrapping array operations with structuredClone results in a semantic change to your getters. The returned arrays will now be independent copies, any changes to them won't be reflected in the original state, and vice versa.

Alternatives

  • If your state's array only consists of primitives, you may prefer to use spread syntax like this: [...state.shallowArray].sort(sortFn). This creates a shallow copy of your array.
  • If structuredClone does not meet your requirements, feel free to use any other cloning method or library like lodash's _.cloneDeep or klona. Example:
    import { cloneDeep } from 'lodash';
    
    // ...
    getters: {
        sortedArray: (state) => cloneDeep(state).sort(sortFn), // valid 
    }
    // ...

Demo

The repo provides a demonstration of the rule.

  • Clone the repo
  • Navigate to the demo folder
  • Install via npm install
  • Lint via npm run lint
  • You will see multiple errors reported by the rule
  • More information can be found in /demo/README.md.

Motivation

After one too many hard-to-track reactivity issues, maximum call stack size exceeded errors and infinite recursion loops, I am no longer willing to spend time endlessly debugging code just to remember at some point that .sort modifies arrays in place.

Contributing

Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request.

License: ISC

© Fabian Mohr, 2024. All rights reserved.

Package Sidebar

Install

npm i eslint-plugin-pinia-getters-no-array-mutations

Weekly Downloads

22

Version

1.2.0

License

ISC

Unpacked Size

68.8 kB

Total Files

14

Last publish

Collaborators

  • fmgrafikdesign