Skip to content

Latest commit

 

History

History
261 lines (191 loc) · 6.26 KB

no-mut-helper.md

File metadata and controls

261 lines (191 loc) · 6.26 KB

no-mut-helper

Reasons to not use the mut helper

  1. General problems in the programming model:

    • The mut helper is non-intuitive to use, see, teach, and learn since it can either be a getter or a setter based on the context in which it’s used.

    Example:

    {{#let (mut this.foo) as |foo|}}
      <!-- When used like this, it's a getter -->
      {{foo}}
    
      <!-- When used like this, it's a setter -->
      <button {{action foo 123}}>Update Foo</button>
    {{/let}}
    • The need for the no-extra-mut-helper-argument rule is further evidence that mut has a non-intuitive signature and frequently gets misused.
    • The mut helper is usually only used as a pure setter, in which case there are other template helpers that are pure setters that could be used instead of mut (e.g. ember-set-helper).
  2. Incompatibility with Glimmer Component intentions:

    • The mut helper can re-introduce 2 way data binding into Glimmer Components on named arguments where a child can change a parent’s data, which goes against the Data Down Actions Up principle, goes against Glimmer Components’ intention to have immutable arguments, and is discouraged by the Ember Core team.

Example:

<input
  type="checkbox"
  checked={{@checked}}
  {{on "change" (fn (mut @checked) (not @checked))}}
/>

What this rule does

This rule forbids any use of the mut helper, both as a getter and a setter, in any context. It also surfaces possible alternatives in the lint violation message to help guide engineers to resolving the lint violations.

Examples

This rule forbids the following:

<MyComponent @isDropdownOpen={{mut this.isDropdownOpen}}/>
<button {{action (mut this.isDropdownOpen) false}}>Close Dropdown</button>
<MyComponent @closeDropdown={{action (mut this.isDropdownOpen) false}}/>
<MyComponent onclick={{fn (mut this.isDropdownOpen) false}}/>
<Input onchange={{action (mut this.profile.description) value="target.value"}}/>
{{my-component click=(action (mut this.isDropdownOpen) false)}}
{{my-component value=(mut this.profile.description)}}

This rule allows the following:

<MyComponent @isDropdownOpen={{this.isDropdownOpen}}/>
<button {{action (set this "isDropdownOpen" false)}}>Close Dropdown</button>
<button {{on "click" (set this "isDropdownOpen" false)}}>Close Dropdown</button>
<button {{on "click" (fn this.setIsDropdownOpen false)}}>Close Dropdown</button>
<button {{action "setIsDropdownOpen" false}}>Close Dropdown</button>
<MyComponent @closeDropdown={{action this.setIsDropdownOpen false}}/>
<MyComponent onclick={{fn this.setIsDropdownOpen false}}/>
<Input onchange={{action this.setProfileDescription}}/>

Coupled with a corresponding JS action to set:

@action
setProfileDescription({ target: { value } }) {
  set(this, 'profile.description', value);
}
{{my-component click=(action (set this "setIsDropdownOpen" false)}}
{{my-component value=this.profile.description}}

Migration

  1. When used as a pure setter only, mut could be replaced by a JS action ("Option 1" below) or ember-set-helper ("Option 2" below):

Before:

<MyComponent
  @closeDropdown={{action (mut this.setIsDropdownOpen) false}}
/>

After (Option 1 HBS):

<MyComponent
  @closeDropdown={{action this.setIsDropdownOpen false}}
/>

After (Option 1 JS):

@action
setIsDropdownOpen(isDropdownOpen) {
  set(this, 'isDropdownOpen', isDropdownOpen);
}

After (Option 2):

<MyComponent
  @closeDropdown={{set this "isDropdownOpen" false}}
/>


2. When used as a pure getter only, mut could be removed:

Before:

<MyComponent
  @isDropdownOpen={{mut this.isDropdownOpen}}
/>

After:

<MyComponent
  @isDropdownOpen={{this.isDropdownOpen}}
/>


3. When mut is used as a getter and setter, mut could be replaced with a different namespace for the property and a dedicated action function to set the property: (Note: another other option could be to pull in the pick helper from ember-composable-helpers and use it like this.) (Note: Another option could be to use ember-box).

Before:

{{#let (mut this.foo) as |foo|}}
  {{foo}}
  <input onchange={{action foo value=target.value}} />
{{/let}}

After HBS:

{{this.foo}}
<input {{onchangethis.updateFoo}} />

After JS:

@tracked
foo;

@action
updateFoo(evt) {
  this.foo = evt.target.value;
  // or set(this, ‘foo’, evt.target.value); for legacy Ember code
}


4. When mut is being passed into a built-in classic component that uses 2 way data binding, mut could be removed:

Before:

<Input
  @value={{mut this.profile.description}}
/>

After:

<Input
  @value={{this.profile.description}}
/>

Configuration

  • object -- containing the following properties:
    • string -- setterAlternative -- Optional: String name of a helper that could replace mut as a setter. If configured, the lint violation error message will include this as a possible alternative for resolving the lint violation.

Example:

// .template-lintrc.js

module.exports = {
  rules: {
    'no-mut-helper': ['error', {
      setterAlternative: '{{set}}',
    }]
  }
};

Related Rules

References