iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
自我挑戰組

程式碼 AI 寫,我負責看懂:30 天拆解 React × TypeScript系列 第 11

Day 11|為什麼 React 不讓我直接改資料?認識 State 更新

  • 分享至 

  • xImage
  •  

Vue 可以直接 push(),React 為什麼不行?

上一篇認識 useState() 時,知道 React 需要透過更新函式改變 State:

const [todos, setTodos] = useState<Todo[]>([])

其中能看到出現了一個比較陌生點的寫法:

setTodos([...todos, newTodo])

過去在 Vue 專案中,可以直接使用 push() 新增資料:

// Vue
todos.value.push({
  id: Date.now(),
  text,
  completed: false,
})

為什麼到了 React,不能直接寫成:

todos.push(newTodo)

而是要建立一個新陣列,再交給 setTodos()

因為 React 會將 State 視為唯讀資料。更新陣列或物件時,不應該直接修改原本的內容,而是要建立一份新的資料,再透過更新函式交給 React。

什麼是直接修改資料?

JavaScript 的陣列本身可以被修改,例如:

const todos = []

todos.push(newTodo)

push() 會直接改變原本的 todos 陣列。這種直接改變原資料的操作,通常稱為 Mutation。
常見的陣列 Mutation 方法包括:

push()
pop()
splice()
sort()
reverse()

在一般 JavaScript 中,這些方法不是不能使用;但如果陣列被放在 React State 裡,就不應該直接修改它。

例如:

todos.push(newTodo)
setTodos(todos)

雖然最後呼叫了 setTodos(),但傳回去的仍然是原本那個陣列。
因為傳入的仍然是原本那個陣列,React 會將它判斷為相同的 State,因此可能略過這次畫面更新。直接修改原資料,也容易讓程式中的資料變化變得難以追蹤。

所以更適合的做法是:

不修改原本的 State,而是根據舊資料建立一份新資料,再交給更新函式。

React 官方文件:更新 State 中的陣列

新增 Todo:使用展開運算子

目前專案新增 Todo 的程式碼是:

function addTodo(text: string) {
  const newTodo: Todo = {
    id: Date.now(),
    text,
    completed: false,
  }

  setTodos([...todos, newTodo])
}

這裡的:

[...todos, newTodo]

會建立一個新陣列。

...todos 會先將原本的 Todo 放進新陣列,再將 newTodo 放在最後面。

假設原本的資料是:

[todoA, todoB]

執行:

[...todos, newTodo]

就會得到:

[todoA, todoB, newTodo]

原本的 todos 沒有被直接修改,而是產生一個包含新項目的陣列,再交給 setTodos()

刪除 Todo:使用 filter()

function deleteTodo(id: number) {
  setTodos(todos.filter((todo) => todo.id !== id))
}

filter() 不會直接修改原陣列,而是根據條件建立一個新陣列。

這裡的條件:

todo.id !== id

代表只保留 id 不等於刪除目標的 Todo。

因此,這段程式碼可以理解成:

建立一個不包含指定 Todo 的新陣列,再使用它更新 State。

修改 Todo:使用 map()

function toggleTodo(id: number) {
  setTodos(
    todos.map((todo) =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo,
    ),
  )
}

map() 會建立一個新陣列。

如果目前的 Todo 是要修改的資料:

todo.id === id

就回傳一個新物件:

{ ...todo, completed: !todo.completed }

其中,...todo 會保留原本 Todo 的內容,再將 completed 改成相反的值。

如果不是要修改的 Todo,就直接回傳原本的資料:

todo

整段可以理解成:

找到指定 Todo → 建立一個更新後的新物件
其他 Todo     → 保持原本內容

最後,map() 將結果組成新陣列,再交給 setTodos()

為什麼陣列和物件都要建立新的?

只建立新陣列,還不一定代表裡面的物件也是新的。

例如:

const nextTodos = [...todos]
nextTodos[0].completed = true

雖然 nextTodos 是一個新陣列,但裡面的 Todo 物件仍然和原本陣列指向同一份資料。

因此:

nextTodos[0].completed = true

還是會修改到原本 State 裡的 Todo。

這也是專案在修改 Todo 時,同時使用兩種寫法的原因:

todos.map(...)

負責建立新陣列,而:

{ ...todo, completed: !todo.completed }

則負責建立更新後的新 Todo 物件。

可以先將原則記成:

從實際要修改的那一層開始,建立新的資料。

State 是每次 Render 的快照

React 官方文件將 State 描述成某一次 Render 的快照。

例如:

setText('新的文字')
console.log(text)

呼叫 setText() 後,不會立刻改變目前這次程式執行中的 text

setText() 比較像是在通知 React:

下一次 Render 請使用新的 State。

接著讀取 text 時,仍然可能取得這次 Render 的舊值。到了下一次 Render,Component 才會收到新的 State,並使用它更新畫面。

React 官方文件:State 就像一張快照

這也說明了為什麼 State 不像一般變數一樣,直接指定新的值就好,而是需要透過 React 提供的更新函式處理。

Vue 和 React 更新陣列的差異

// Vue
todos.value.push(newTodo)
// React
setTodos([...todos, newTodo])

Vue 的響應式系統可以追蹤對陣列進行的修改,因此可以使用 push() 更新 ref() 裡的陣列。
React 則將 State 視為唯讀資料,需要建立新陣列或新物件,再透過更新函式設定新的 State。
兩者都是為了讓資料改變後更新畫面,只是採用的方式不同。

常見的陣列更新方式

在 React State 中,可以先這樣整理:

操作 避免直接使用 建議使用
新增 push() [...todos, newTodo]
刪除 splice() filter()
修改 直接指定陣列項目 map()
排序 直接使用 sort() 先複製陣列再排序

重點不是這些 JavaScript 方法突然不能使用,而是:

不要直接修改已經存在於 State 裡的資料。

今日回顧

  • React 將 State 視為唯讀資料,不應該直接修改,而是要透過更新函式處理
  • 新增、刪除、修改都要先建立新陣列,分別對應展開運算子、filter()map()
  • 修改陣列中的物件時,也要建立新的物件,而不是只複製外層陣列
  • State 是某一次 Render 的快照,呼叫更新函式後,新的 State 要到下一次 Render 才會出現

這篇算是上一篇的再深入延伸,下一篇預計往 React 的事件處理探索,下篇見~


上一篇
Day 10|ref() 變成 useState()?兩個真的可以直接畫等號嗎?
下一篇
Day 12|React 事件處理跟 Vue 之間的不同
系列文
程式碼 AI 寫,我負責看懂:30 天拆解 React × TypeScript22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言