Here's how I write typical React components:
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount((c) => c + 1)
return <button onClick={increment}>{count}</button>
}
Notice how I mix arrow functions and function declarations. The number of questions I get from people about this might surprise you. When I get a lot of questions about a subject, I blog about it, so here we go with that.
To be totally clear, there are four types of functions I'll discuss in this post:
- Function declarations
- Function expressions
- Arrow functions
- Object methods
function functionDeclaration() {
// do stuff
// return stuff
}
// using var because I'm feeling retro
var functionExpression = function () {
// do stuff
// return stuff
}
var functionExpressionWithName = function someName() {
// do stuff
// return stuff
}
const arrowFunction = () => {
// do stuff
// return stuff
}
const arrowFunctionWithImplicitReturn = () => 'return stuff'
const obj = {
functionExpressionProperty: function () {},
arrowFunctionProperty: () => {},
objectMethodFunction() {},
}
Each has slightly different semantics which I will not belabor in this post.
I started coding JavaScript before arrow functions were a real thing. So I got used to using function declarations and function expressions. I developed some loose "rules" for myself for when to use one over the other. Here they are:
- I use a function expression if I'm passing the function as a callback.
- I use a function expression if I'm setting it to the property of an object.
- I use a function declaration every other time.
The reason I preferred function declarations is because of a simple feature of declarations over expressions: hoisting of the function definition.
thisWorks()
function thisWorks() {}
thisThrowsAnError()
var thisThrowsAnError = function () {}
The error thrown was our favorite
Uncaught TypeError: undefined is not a function
(though these days it would be
Uncaught TypeError: thisThrowsAnError is not a function
because JS engines
have gotten better at this kind of thing).
Remember, these are loose rules and I didn't always follow them, nor do I think they're worth enforcing via an ESLint rule or anything.
When arrow functions and object methods came around, my loose rules changed slightly:
- I use an arrow function if I'm passing the function as a callback.
- I use object methods when the function is multiple lines or I don't want to return anything, otherwise I use an arrow function
- I use an arrow function if I want to leverage the implicit return or lexical
scope feature (
this
works the way you normally want it to). - I use a function declaration every other time.
And sometimes I'll make single-line, returning functions a function declaration
anyway, just because it's easier to stick in a debugger statement or
console.log
.
These aren't really hard and fast rules or anything. There are more reasons to use different forms of functions. In fact, it's arguably better to use function expressions for callbacks because then you can give the function a name which will help debugging (so I'll do that sometimes for that reason).
Anyway, I hope that was interesting!