Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce default parameters to be last (default-param-last) #1414

Closed
feross opened this issue Sep 14, 2019 · 14 comments
Closed

Enforce default parameters to be last (default-param-last) #1414

feross opened this issue Sep 14, 2019 · 14 comments

Comments

@feross
Copy link
Member

feross commented Sep 14, 2019

https://eslint.org/docs/rules/default-param-last

Putting default parameter at last allows function calls to omit optional tail arguments.

// Correct: optional argument can be omitted
function createUser(id, isAdmin = false) {}
createUser("tabby")

// Incorrect: optional argument can **not** be omitted
function createUser(isAdmin = false, id) {}
createUser(undefined, "tabby")

Rule Details

This rule enforces default parameters to be the last of parameters.

Examples of incorrect code for this rule:

/* eslint default-param-last: ["error"] */

function f(a = 0, b) {}

function f(a, b = 0, c) {}

Examples of correct code for this rule:

/* eslint default-param-last: ["error"] */

function f(a, b = 0) {}
@mightyiam
Copy link
Member

Would be nice to confirm that our tested-against users have some default parameters.

@feross feross modified the milestones: standard 15, standard 16 Oct 22, 2020
@feross
Copy link
Member Author

feross commented Oct 29, 2020

Only 1 failing repo. This will ship in standard 16!

@feross feross closed this as completed Oct 29, 2020
feross added a commit to standard/eslint-config-standard that referenced this issue Oct 29, 2020
@Kjaer
Copy link

Kjaer commented Nov 11, 2020

Whoever dropped this issue later,

I've got problem on this because I am using redux and order of arguments matter on reducers,

this is very common reducer I am using currently and violation of the default-param-last rule

export function reducer (state = [], action) {...}

in order to disable only default-param-last but apply the rest set of rule. I am using eslint's overrides

this example from my .eslintrc.json

"overrides": [
    {
      "files": [
        "./src/reducers/**/*.js"
      ],
      "rules": {
        "default-param-last" : "off"
      }
    }
  ]

@uniqname
Copy link

I agree with @Kjaer that this flies in the face of many accepted patterns. This was a bad call. Having default values indicates more than just optionality. It is also a signal to developers and tooling of the expected type. It assumes too much that an argument with a default value is simply optional. I strongly urge that this be removed as part of Standardjs.

@feross
Copy link
Member Author

feross commented Nov 11, 2020

@Kjaer How do you call the reducer? Can you share some examples? Am I correct in assuming that you should always call it with two arguments? In which case, why are you using a default argument?

@uniqname Having default values indicates more than just optionality. It is also a signal to developers and tooling of the expected type

Which tooling works this way?

@Kjaer
Copy link

Kjaer commented Nov 12, 2020

@feross unfortunately it's not about how I call reducers, It's about how Redux as a lib wants me write the reducers.
My issue started to happen when I tried to combine reducers

https://redux.js.org/api/combinereducers#reducerstodosjs

You can check Redux's combineReducers test in order to confirm the order of arguments matter

https://github.com/reduxjs/redux/blob/master/test/combineReducers.spec.ts#L14

It's common (maybe best) practice to assign default values to state arguments on reducers based on their examples, test and docs.

@feross
Copy link
Member Author

feross commented Nov 13, 2020

You can check Redux's combineReducers test in order to confirm the order of arguments matter

Oh totally, I understand that the order of arguments will matter to Redux. My question was would you ever invoke a reducer without any arguments, because this is the only circumstance in which the first default argument would take effect.

For example, you could take this:

export default function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

And you could rewrite it as:

export default function todos(state, action) {
  ...

There's no need to re-order the arguments. You can just remove the default argument. Unless you or Redux itself is calling todos() (without arguments) somewhere.

If that is the case, then you'd need to rewrite it as:

export default function todos(state = [], action = undefined) {
  ...

Which I agree is not ideal. If this is the case, then I'm sympathetic to disabling this rule.

@feross feross reopened this Nov 13, 2020
@falmar
Copy link
Contributor

falmar commented Nov 17, 2020

When redux initializes it does call all reducers without a state...

It's common to create reducers with initial state as (state = initialState, action) without an initial state, it does results in an error

Reducer "..." returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.

When using Duck Bundles

it makes more sense for these pieces to be bundled together in an isolated module that is self-contained

Redux's createStore() accepts a second argument for a preloadedState but it becomes cumbersome to import initialStates from multiple modules/files, and most often the preloadedState comes from server-side rendering and not always has any value or is complete

// redux/modules/counter.js
export default (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}
// redux/modules/index.js
import { combineReducers } from 'redux'

import counter from '@redux/modules/counter'
// .. more imports

export default combineReducers({
  counter
 // .. more reducers
})
// redux/configureStore.js
import { createStore } from 'redux'

import rootReducer from '@redux/modules'

export default  (preloadedState) => createStore(rootReducer, preloadedState)
// src/index.js 
// react imports

import configureStore from '@redux/configureStore.js'

const preloadedState = window.__PRELOADED_STATE__ || {}
const store = configureStore(preloadedState)

// 
ReactDOM.render(...)
// ...

This rule would enforce dropping the convenience of having that default parameter, but to be fair when dealing with SSR preloadedState hydrating the is required

// constants
const CHANGE_VIEWPORT = '.../viewport/CHANGE_VIEWPORT'

// action creators
export const changeViewport = payload => ({
  type: CHANGE_VIEWPORT,
  payload: payload
})

// reducer
const initialState = {
  hydrated: false,
  height: null,
  width: null
}

export default (state, action) => {
  if (!state || !state.hydrated) {
    state = {
      ...initialState,
      ...state,
      hydrated: true
    }
  }

  switch (action.type) {
    case CHANGE_VIEWPORT:
      return {
        ...state,
        ...action.payload
      }

    default:
      return state
  }
}

Sorry for the long code examples.

@sQVe
Copy link

sQVe commented Nov 17, 2020

My opinion is that this rule is way too opinionated to be set as a default. I just ran it against a Redux based project and got around 200 errors.

There's just too many cases where you'd like to set defaults to a parameter at the head like:

const defaultPosition = { x: 0, y: 0 }
const addPosition = (position = defaultPosition, options) => {}

Adding undefined as a default to options here just hurts readability.

Edit

Furthermore, this is very problematic if you introduce defaults to an already existing API. It would force you to either opt out from the rule on every case or do a far larger refactor.

@feross
Copy link
Member Author

feross commented Nov 17, 2020

I'm now leaning toward disabling this rule. @standard/team Do you have any feedback before I do that?

@ungoldman
Copy link
Contributor

ungoldman commented Nov 17, 2020

As it's not a syntax error or a major stylistic problem, it seems arbitrary, so I would lean strongly towards disabling.

I can also think of plenty of cases like the above ones where argument order is not in control of the user, but default parameters are useful, and this rule would just get in the way of people getting the job done.

@mcollina
Copy link

I am in agreement with @feross, +1 to disable.

@mixmix
Copy link

mixmix commented Nov 18, 2020

Keen to disable - this rule doesn't currently work well with callback style functions:

get (id, opts = {}, cb) 

feross added a commit to standard/eslint-config-standard that referenced this issue Nov 18, 2020
@feross
Copy link
Member Author

feross commented Nov 18, 2020

Released as 16.0.3.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
Archived in project
Development

No branches or pull requests

9 participants