ReactJS

React vs Svelte 開發體驗:子元件暫時更新父層資料

當程式規模慢慢變大時,為了管理與維護會將大元件拆分成許多小元件,在父子元件之間傳遞資料是開發時很常見的。但在傳遞資料上,React 跟 Svelte 沒什麼區別,同樣都是將資料傳入元件的 attribute 中,子元件透過 props 來獲得父層傳入的資料。所以我們這章舉一個比較複雜一點的例子:讓子元件可以暫時更新資料,但如果父元件的資料更新時,子元件也要更新為父元件元件的值。我們將透過範例比較兩者在開發體驗上的差異。

開發體驗

開發者在使用工具、框架、語言、API、系統或流程時,整體的感受、效率、流暢度與滿意度。

使用情境

當你在開發表單、步驟流程或是可編輯區塊時,子元件常會需要「暫時修改」父元件提供的資料。例如使用者輸入值,但尚未儲存前,這些變更應該是本地的。這時候就需要讓子元件在不直接修改父層狀態的前提下,保有一定程度的控制權。

React:需要手動管理狀態同步

React 採用單向資料流設計,而且我們無法直接修改 props。在子元件想要暫時更新資料時,必須透過 useState 加上 useEffect 來同步 props 的變化 (demo)。

ParentComponent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

function ParentComponent() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>
clicks (parent): {count}
</button>
<ChildComponent count={count} />
</div>
);
}

export default ParentComponent;

ChildComponent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect } from 'react';

function ChildComponent({ count }) {
const [localCount, setLocalCount] = useState(count); // create a local count for temporary use

useEffect(() => {
setLocalCount(count);
}, [count]);

const increment = () => {
setLocalCount(localCount + 1);
};

return (
<button onClick={increment}>
clicks (child): {localCount}
</button>
);
}

export default ChildComponent;

在這個範例中:

  • 子元件透過 useState 創建一個本地的 localCount,用來「暫時更新」畫面上的計數值。
  • 當父元件的 count 改變時,useEffect 會同步更新 localCount
  • 子元件的操作不會影響父元件的實際資料。

Svelte:直覺操作、語法簡潔

Svelte 允許我們在子元件中直接修改 props,而不需要額外建立本地狀態。這種語法大幅簡化了開發流程,對開發者非常友善 (demo)。

Parent.svelte

1
2
3
4
5
6
7
8
9
10
11
<script>
import Child from './Child.svelte';

let count = 0;
</script>

<button onclick={() => (count += 1)}>
clicks (parent): {count}
</button>

<Child {count} />

Child.svelte

1
2
3
4
5
6
7
<script>
let { count } = $props();
</script>

<button onclick={() => (count += 1)}>
clicks (child): {count}
</button>
  • 無需 useEffect 或本地狀態。
  • 可直接操作傳入的 count,操作簡單明瞭。
  • 預設變更僅限於子元件本地,不會同步到父層(除非你明確綁定 bind: 或使用事件)。

以上 React 與 Svelte 兩個例子功能相同,但 Svelte 的語法設計,讓開發者可以更快速專注在邏輯本身,而非額外的狀態管理機制。

延伸閱讀

React vs Svelte 開發體驗:雙向資料綁定

資料綁定 (Data Binding) 是連結應用程式資料 (Application) 與使用者介面 (UI) 的方式或機制。React 是使用單向資料綁定 (one-way data flow),需手動呼叫 setState 來更新狀態;而 Svelte 使用 bind: 指令來達成雙向資料綁定。我們將透過範例來了解兩者在開發體驗上的差異。

開發體驗

開發者在使用工具、框架、語言、API、系統或流程時,整體的感受、效率、流暢度與滿意度。

使用情境

當你在開發表單、表單輸入與使用者互動或即時資料顯示,需要反向更新狀態變數,並即時顯示給使用者 (e.g. 動態價格計算、表單即時驗證、即時預覽)。

React:單向資料綁定

React 採用單向資料流 (one-way data flow),透過手動呼叫 setstate 來更新狀態。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from 'react';

function InputComponent() {
const [text, setText] = useState('');

return (
<>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>{text}</p>
</>
);
}
  • useState 宣告狀態變數 text,初始值為空字串。
  • value={text} 綁定輸入框顯示值。
  • onChange 在輸入變動時更新 text

Svelte:直覺式雙向綁定

Svelte 提供了 bind: 指令,簡化雙向資料綁定,讓變數與元件保持同步。

1
2
3
4
5
6
<script>
let text = '';
</script>

<input type="text" bind:value={text} />
<p>{text}</p>
  • 使用 let 宣告響應式 (reactivity) 變數 text
  • bind:value={text} 自動雙向綁定輸入值與變數。
  • 使用者輸入會更新 text,變數更新也會同步回輸入框。

哲學

React 與 Svelte 在資料綁定上的差異,反映了各自的框架哲學:

  • React 注重控制與可預測性,適合處理複雜應用邏輯與資料流。
  • Svelte 注重簡潔與開發效率,讓使用者開發時更貼近原生思維。

延伸閱讀

React vs Svelte 開發體驗:狀態管理

在管理資料狀態上,React 採用的是 useState Hook,而 Svelte 則是直接使用變數,使用起來更直觀。我們將透過範例來了解兩者在開發體驗上的差異。

開發體驗

開發者在使用工具、框架、語言、API、系統或流程時,整體的感受、效率、流暢度與滿意度。

React 狀態管理:使用 useState Hook

React 讓元件可以擁有內部狀態的方式是透過 useState Hook 來完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
Counter:{count}
</button>
);
}

export default Counter;
  • useState(0):初始化 count 為 0。
  • setCount:更新 count 的函式,每次按鈕點擊讓值加 1。
  • 透過 React 的 Virtual DOM,變化的值會觸發元件重新渲染。

非同步更新注意事項

React 的狀態更新是非同步的,因此如果在同一函式中連續呼叫多次 setCount(count + 1),可能無法即時反映最新的狀態。建議改用以下方式來避免此問題:

1
setCount(prevCount => prevCount + 1);

這樣可以保證邏輯永遠是基於最新狀態更新。

Svelte 狀態管理:響應式變數

Svelte 採用一種更直覺的狀態處理方式。它不需要額外的 Hook,直接使用變數即可自動觸發畫面更新。

1
2
3
4
5
6
7
<script>
let count = 0;
</script>

<button on:click={() => count++}>
Counter:{count}
</button>
  • 使用 let count = 0 宣告變數。
  • 直接對 count 進行操作,例如 count++,Svelte 就能自動感知變化並更新 DOM。
  • 這是透過 Svelte 的 編譯時響應式系統 (reactivity system) 實現的,完全不需要 Virtual DOM。

Note: React.js 使用 Virtual DOM 來追蹤是否元件需要被更新;而 Svelte 沒有使用 Virtual DOM 而是直些更新在 DOM 上。

React vs Svelte 狀態更新差異

功能面向 React Svelte
語法複雜度 使用 Hook,語法稍多 直覺操作變數,語法簡潔
DOM 更新機制 Virtual DOM 比對再更新 編譯階段生成 DOM 操作
狀態更新方式 必須使用 setState / setCount 直接修改變數
更新非同步處理 是,需注意資料一致性 否,變數改變即更新
可讀性與維護性 中等

如果追求極簡語法與優化效能,Svelte 提供了非常流暢的開發體驗。而 React 則擁有強大的生態系與社群支持。

延伸閱讀

何時該使用 React Redux

今天這章會討論什麼時候要把資料放在 Redux 裡面,什麼時候要將資料放在 React 的 components 裡。

React 將 components 分成 Presentational Components (展示組件) 與 Container Components (容器組件)。

Presentational Components

Presentational Components 主要負責 UI 的部分,通常不會有複雜 application 狀態管理的資料,通過會由 props 傳入無狀態 (stateless) 的資料,像是:

  • 跟 UI 相關的資料 e.g. 顏色的使用
  • 只有在必要時才擁有自己的狀態 e.g. 下拉式選單的狀態
  • 需要手動建立新的東西 e.g. new post 的資料儲存

Container Components

Container Components 主要負責 application 的狀態,通常會使用 Redux 來管理,然後在 render 的時候會資料傳給 Presentational Components,通常會儲存有狀態的資料:

  • application data flow
  • 使用者相關的資訊 e.g. 最喜歡的顏色

Compare

特性 Presentational Container
主要用途 UI render application state
狀態 無狀態或是透過 props 獲得狀態 通常有狀態,透過 Redux 管理
類別 smart dumb
儲存類型 儲存複雜的東西 儲存簡單的東西

Reference

React Redux Example App 範例程式碼

前一篇介紹 Redux 是參考 Flux 的架構而設計的,這篇要用 Redux 來寫一個簡單的 React Counter App。

在 Ubuntu 18.04 上安裝 Docker CE

再提醒一下,React 跟 Redux 的差別是:React 是一個 front-end library;Redux 是一個架構,可以不用跟 React 一起使用,也可以跟 Vue 或 Angular 搭配。

Counter App

Install

建立一個 React App 然後安裝 Redux:

1
2
3
npx create-react-app counter-app
cd counter-app
npm install --save redux react-redux

Actions

建立 Redux 的 Action,Action 一定要回傳 type 讓 Reducer 去做對應的資料更新,Middleware 會在送到 Reducer 之前執行。

1
2
3
4
5
6
7
8
// actions.js
export const increment = () => ({
type: 'INCREMENT'
});

export const decrement = () => ({
type: 'DECREMENT'
});

Reducer

建立 Redux 的 Reducer,Reducer 不能修改原本的 state,必須複製一份,並且回傳最後 state 應該要的資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// reducer.js
const initialState = {
count: 0
};

const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
};
case 'DECREMENT':
return {
...state,
count: state.count - 1
};
default:
return state;
}
};

export default counterReducer;

Store

建立 Redux 的 Store,用來儲存 state 的資料

1
2
3
4
5
6
7
// store.js
import { createStore } from 'redux';
import counterReducer from './reducer';

const store = createStore(counterReducer);

export default store;

Counter

建立 React Counter Element,而且注意這邊需要將 Action 傳入 Dispatcher 來讓後續的 Reducer 來更新狀態。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

const Counter = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();

return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
};

export default Counter;

App

最後 react-redux library 透過 Provider Component 來跟 React 串接,Provider 會提供 Store 的資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';

const App = () => (
<Provider store={store}>
<Counter />
</Provider>
);

export default App;

Reference

Flux 跟 Redux 之間的關係

Flux 和 Redux 都是用來管理 application 狀態的架構,差異是不同的設計理念和實做方式:

Flux

Flux 是 Facebook 提出的架構模式,用於解決複雜的 data flow 問題,Flux 的架構如下:

  1. Action: 描述 application 的事件或行為。
  2. Dispatcher: 分發 Action 給 Store。它是 Flux 架構中的中央樞紐。
  3. Store: 儲存 application 狀態和邏輯。每個 Store 負責 application 一部分狀態。
  4. View: 展示 application UI,並且可以根據 Store 的變化來更新。

Flux 的 data flow 是單向的,從 Action 到 Dispatcher,再到 Store,最後到 view。

Redux

Redux 參考並簡化 Flux 管理狀態概念,Redux 的架構如下:

  1. Action: 與 Flux 中的 Action 類似,用於描述 application 的事件或行為。
  2. Reducer: 是一個 function,負責根據 Action 來更新 application 的狀態。Redux 中沒有 Sispatcher,取而代之的是 Reducer。
  3. Store: 儲存 application 的狀態。Redux 中只有一個單一的 Store,與 Flux 有多個 Store 不同。而且 Redux 只能透過 Action 可以修改 Store 的資料。
  4. Middleware: 用於處理異步操作或其他事件 e.g. Error Handling。

Redux 的 data flow 也是單向的,並且強調使用 function 來更新狀態。

Redux 使用單一集中狀態的物件,並以特定的方式進行更新。當你想要更新狀態時(e.g click event),會創建一個 Action 並由某個 Reducer 處理。Reducer 會複製當前狀態,且使用 Action 中的資料進行修改,然後返回新的狀態。當 Store 更新時,可以監聽事件並更新

Redux 跟 React 的差別是:React 是一個 front-end library;Redux 是一個架構,可以不用跟 React 一起使用,也可以跟 Vue 或 Angular 搭配。這篇是一個 React Redux 的範例:

React Redux Example App 範例程式碼

Compare

  • Redux 使用單一的 store: 與 Flux 在中多個 Store 中定位狀態信息不同,Redux 將所有內容保存在一個地方。在 Flux 中,可以有許多不同的 store。Redux 打破了這一點,強制使用單一 global Store。
  • Redux 使用 reducers: Reducers 是以不更動原本資料的方式來更新資料。在 Redux 中,行為是以可預測的,因為所有的改動都要經過 Reducer,且每一次的改動只會更新一次 global Store。
  • Redux 使用 middleware: 由於 Action 和資料以單向方式流動,我們可以透過 Redux 增加 middleware,並在資料更新時加上客製化的行為 e.g. Log or Catch Error。
  • Redux decouple Action 與 Store: 建立 Action 時不會向通知 Store 任何東西,反而是回傳 Action 物件;Flux 的 Action 則會直接修改 Store。

Reference