iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
JavaScript

JavaScript為什麼筆記本系列 第 17

Day 17|onClick={login} 為什麼不用寫成 login()?

  • 分享至 

  • xImage
  •  

Day 16 最後,我們先把登入按鈕寫成這樣:

<button
  onClick={() => {
    setUser({
      name: 'Anthony'
    })
  }}
>
  Sign In
</button>

當時我先把它理解成:

按下 Sign In
↓
執行 setUser(...)
↓
user 更新
↓
React 重新 render

這樣用沒有問題。

onClick={() => { ... }}

為什麼中間要多一個 () => {}

不過我們要的不就是「按下去時執行 setUser()」嗎?

那直接這樣不行嗎?

onClick={setUser({
  name: 'Anthony'
})}

先給最短答案:差別在「現在執行」還是「之後執行」

先看三個長得很像的東西:

login
login()
() => login()

它們其實不是同一件事。

login
→ login 這個 function 本身

login()
→ 現在立刻呼叫 login

() => login()
→ 建立一個新的 function
→ 這個新 function 之後被呼叫時,才執行 login()

而 React 的 onClick 要的,正是:

一個可以等到 click 發生時再被呼叫的 function。

所以今天真正要看懂的,不是 Arrow Function 長什麼樣。

而是這兩個括號:

login

和:

login()

到底差在哪裡。


先不要碰 setUser,自己寫一個最簡單的 function

先寫:

function login() {
  console.log('登入')
}

我希望做到:

按 Sign In
↓
執行 login
↓
Console 出現「登入」

React 可以這樣寫:

<button onClick={login}>
  Sign In
</button>

第一次看到這裡其實很怪。

因為平常我要執行 function,不是都寫:

login()

嗎?

為什麼這裡反而少了 ()

答案是:因為現在還不要執行。

這時候我只是先把 login 這個 function 交給 React。

可以先想成:

React render
↓
拿到 login 這個 function
↓
先不執行
↓
使用者真的 click
↓
React 呼叫 login

所以:

onClick={login}

不是少寫了括號。

而是故意不寫括號。


做一個最小實驗:到底什麼時候執行?

這一篇很適合直接看 Console,不然很容易只是把答案背起來。

先寫:

function App() {
  console.log('1. App render')

  function login() {
    console.log('2. login 執行')
  }

  return (
    <button onClick={login}>
      Sign In
    </button>
  )
}

你會先看到:

1. App render

這時候還不應該因為 onClick={login} 就看到:

2. login 執行

等真的按下 Sign Inlogin 才會執行。

這就是:

onClick={login}

最重要的行為。

它不是「render 時執行 login」。

而是「render 時把 login 當成事件處理 function 交給 React」。


那如果真的寫成 onClick={login()} 呢?

把剛剛那行改成:

<button onClick={login()}>
  Sign In
</button>

這次事情完全不一樣。

因為 JSX 在 render 時,JavaScript 必須先算出大括號裡這個 expression 的結果:

login()

而看到 (),意思就是:

現在呼叫 login

所以流程會變成:

React render
↓
計算 login()
↓
login 立刻執行
↓
login 的回傳值才會成為 onClick 收到的值

也就是說,我根本還沒按按鈕,login() 就已經跑了。

如果 login() 沒有特別 return 一個 function,那它通常回傳的是 undefined

所以這個:

onClick={login()}

其實比較像:

先把 login 執行掉
↓
再把 login() 的結果交給 onClick

這當然不是我們原本要的「等 click 再登入」。


回到 setUser(),問題就很清楚了

Day 16 的:

setUser({
  name: 'Anthony'
})

本身就是一次 function call。

所以如果寫:

<button
  onClick={setUser({
    name: 'Anthony'
  })}
>
  Sign In
</button>

JavaScript 在 render 這個 Component 時,就會先執行:

setUser({
  name: 'Anthony'
})

setUser 又會要求 React 更新 state。

於是很可能變成:

render
↓
執行 setUser
↓
state 更新
↓
又 render
↓
又執行 setUser
↓
...

這也是為什麼這種寫法不只是「時機不太對」。

它甚至可能直接造成重複 render 的錯誤。

我們真正要的是:

現在 render
→ 先準備好登入這件事情

未來 click
→ 才執行 setUser(...)

原來 () => {} 是在幫我們把執行時間往後推

現在再看原本那段:

onClick={() => {
  setUser({
    name: 'Anthony'
  })
}}

先不要把它看成一大坨 React 語法。

只看這一段:

() => {
  setUser({
    name: 'Anthony'
  })
}

它本身是在建立一個 function

建立 function 不等於執行 function。

所以 render 時發生的是:

建立一個新的 function
↓
把這個 function 交給 onClick
↓
先不執行裡面的 setUser

真的 click 之後才會:

React 呼叫這個 function
↓
function 裡面執行 setUser(...)
↓
user 更新

所以 Arrow Function 在這裡最重要的作用,不是「React 規定一定要這樣寫」。

而是:

先建立一個 function,把真正要做的事包在裡面,等 click 時再執行。


其實可以先寫成 login,反而更好懂

原本:

<button
  onClick={() => {
    setUser({
      name: 'Anthony'
    })
  }}
>
  Sign In
</button>

可以先拆成:

function login() {
  setUser({
    name: 'Anthony'
  })
}

再寫:

<button onClick={login}>
  Sign In
</button>

對我現在來說,這版反而比較容易讀。

因為三件事情被拆開了:

login
→ 登入時要做什麼

onClick
→ 什麼時候做

onClick={login}
→ click 的時候,做 login

這比一開始硬背:

onClick={() => { ... }}

有畫面很多。


那為什麼有時候還是需要 Arrow Function?

假設 login 要接收名字:

function login(name) {
  setUser({
    name: name
  })
}

我們希望 click 之後才執行:

login('Anthony')

但不能寫:

onClick={login('Anthony')}

因為這又會在 render 時直接呼叫 login('Anthony')

所以可以包一層:

onClick={() => login('Anthony')}

現在 onClick 拿到的是:

() => login('Anthony')

這個新的 function。

等 click 發生,React 才呼叫它;它再去呼叫:

login('Anthony')

所以可以先記成:

onClick={login}
→ 把現成的 login function 交出去

onClick={() => login('Anthony')}
→ 先建立一個 function 交出去
→ click 後才用指定參數呼叫 login

不是「React event 一律要寫 Arrow Function」。

而是要看:你現在交給 onClick 的到底是不是一個 function。


這三種寫法,我現在終於分得出來了

1. onClick={login}

login
→ function 本身
→ 交給 React
→ click 時再呼叫

2. onClick={login()}

login()
→ 現在立刻呼叫
→ 再把回傳值交給 onClick

一般按鈕事件不是我們要的效果。

3. onClick={() => login('Anthony')}

() => login('Anthony')
→ 建立新的 function
→ 交給 React
→ click 時才執行 login('Anthony')

三個看起來只差幾個符號,但「執行的時間點」完全不同。


把 Day 16 和 Day 17 接回來

現在完整的最小會員範例可以寫成:

import { useState } from 'react'

function App() {
  const [user, setUser] = useState(null)

  function login() {
    setUser({
      name: 'Anthony'
    })
  }

  return (
    <div>
      <p>
        {user ? `Hello, ${user.name}` : '尚未登入'}
      </p>

      <button onClick={login}>
        Sign In
      </button>
    </div>
  )
}

export default App

現在整條流程可以講成:

第一次 render
↓
user 是 null
↓
畫面顯示「尚未登入」
↓
onClick 拿到 login function
↓
等待使用者操作
↓
使用者 click
↓
React 呼叫 login
↓
login 執行 setUser(...)
↓
user 更新
↓
React 再 render
↓
畫面顯示 Hello, Anthony

Day 16 解決的是:

React 要記住什麼資料?

Day 17 補上的則是:

使用者做了什麼事情,才要去改那份資料?

State 和 Event 到這裡才真的接起來。


今天真正修正的一個觀念

我以前看到:

onClick={...}

很容易把它理解成:

大括號裡放「按下去之後要執行的程式碼」。

這句話不夠準。

因為 JSX 在 render 時,本來就會計算大括號裡的 JavaScript expression。

更準確的理解是:

onClick 要拿到一個 function;React 等 click 發生時,再呼叫這個 function。

所以:

onClick={login}

成立,因為 login 是 function。

這個也成立:

onClick={() => login()}

因為 () => login() 也是 function。

但:

onClick={login()}

會先執行 login(),再把它的回傳值交給 onClick

這才是今天那兩個括號真正重要的地方。


如果今天只記得一件事

不要背:

onClick={() => {}}

先記這三行:

login
→ function 本身

login()
→ 現在呼叫 function

() => login()
→ 建立另一個 function,之後才呼叫 login()

再放回 React:

onClick
→ 需要一個之後能被呼叫的 function

這樣下次再看到 Arrow Function,就不是靠背語法了。



上一篇
Day 16|登入狀態為什麼不能只寫成 `const`?先把 `useState` 這一行看懂
下一篇
Day 18|React 怎麼知道要顯示哪個畫面?
系列文
JavaScript為什麼筆記本20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言