iT邦幫忙

2023 iThome 鐵人賽

DAY 5
1
Modern Web

React 走出新手村 系列 第 5

React 走出新手村-深入useState

  • 分享至 

  • xImage
  •  

複習useState

接著前面所講的,我們將幾個常用的hook都再複習一遍,來解構基礎以外你可能沒發現的細節,那麼我們先從最基礎的 useState 來講起。

理解Proxy

有沒有想過,useState 是怎麼來更改 component 裡面的內容呢?

這其中的概念,可以參考 Javascript 裡面的 Proxy,這裡的 Proxy 指的是 Design pattern 裡面的 Proxy pattern,並不是我們常講的 Proxy server喔!Proxy 是 JavaScript 的一個內建原型,它允許你在目標對象(被代理對象)的操作上注入自定義的行為。使用 Proxy,可以在目標對象的屬性取值、賦值、函數呼叫等操作之前或之後執行自定義的處理邏輯。

那我們先來看看 Proxy 怎麼處裡的吧:

// default物件
const defaultObject = {
  name: 'Luciano',
  age: 30
};

// 透過Proxy建立以defaultObject為預設的代理物件
const proxy = new Proxy(defaultObject, {
  get(target, property, receiver) {
    console.log(`你的名字 ${property}`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`更改 ${property} 為 ${value}`);
    return Reflect.set(target, property, value, receiver);
  }
});

// 使用 Proxy 
console.log(proxy.name); // 觸發get輸出 "你的名字 Luciano"
proxy.age = 31; // 觸發set輸出 "更改 age 為 31"

結合virtual dom

前一篇的章節也複習了 React 的 virtual dom,要更改真實的 Dom 之前你會需要一套機制來隔離虛擬 Dom 與真實 Dom,那也就是 Proxy Pattern 代理處理的部分,我們來模擬一下 useState 的組成吧!

// 我們熟知的setState type
type StateSetter<T> = (newValue: T | ((prevValue: T) => T)) => void;

function useState<T>(initialValue: T): [T, StateSetter<T>] {
  let state: T = initialValue;
  // 這邊也可以考慮換成new Set()
  const subscribers: (() => void)[] = [];

  const setState: StateSetter<T> = (newValue: T | ((prevValue: T) => T)) => {
		// 這邊處理帶進來的是callback | value
    const updatedValue = typeof newValue === 'function' ? (newValue as (prevValue: T) => T)(state) : newValue;
    if (state !== updatedValue) {
      state = updatedValue;
      subscribers.forEach(subscriber => subscriber());
    }
  };

  const stateProxy = new Proxy({ value: state }, {
    get(target, property) {
      if (property === 'value') {
        return state;
      } else if (property === 'set') {
        return setState;
      }
    }
  });

  return [stateProxy.value, stateProxy.set];
}

let currentRender: (() => void) | null = null;
// 這裡就是react底層在處理的渲染時機,當然實際上不只一種
// 詳細的話應該等同於class component的渲染時機
// 原諒我只用最簡單的方式處理
// 有興趣的朋友可以參考solidjs作者的直播
// 我有映像他在介紹如何改善渲染時機的段落有示範如何實作底層
function render(component: () => void) {
  currentRender = component;
  if (currentRender) {
    currentRender();
    currentRender = null;
  }
}

// 使用自定義的 useState Hook
const [count, setCount] = useState(0);

function CounterComponent() {
  console.log('Rendering CounterComponent...');
  console.log(`Count: ${count}`);
  return null;
}

// 模擬組件渲染這裡的機制在 React 裡面會更加複雜一點
render(CounterComponent);

// 更新狀態
setCount(prevCount => prevCount + 1);

// 再次渲染通常應該對應的是 componentDidUpdate
render(CounterComponent);

在上面的範例中,我們使用 TypeScript 模擬了一個簡單的 useState 函數,該函數返回一個陣列,包含狀態和更新狀態的函數,另外處理了一個簡單的 render 函數,用於模擬組件的渲染。

注意:這只是一個簡化的腦補,真正的 React useState Hook 會更加複雜,涉及到更新、批量渲染、多個組件的狀態管理等等。

這裡演示 Proxy 在 useState 的角色,以及如何實現簡單的狀態管理機制。了解一點基礎了以後我們來講講面試常常會問到的問題吧!

useState 的運作是同步還是非同步?

了解了底層以後,不難回答出來他是同步的,那是什麼原因會讓人搞混他是非同步運行的呢?讓我們來看看範例吧:

const Search = () => {
  // 這裡為基本的使用情境,當使用useState的function的同時會產生一個array
  // 也就是為什麼我們常常看到範例的使用方式是長這樣了
  const [txt, setTxt] = useState('');
  const [resData, setResData] = useState([]);
  // 這裡示範一個打api的動作,但為獨立的void function
  const txtChange = (event) => {
    setTxt(event.target.value);
    fetch(`/apiUrlsForSearch?${txt}`)
    .then((res) => res.json()).then(setResData)
  }
  // 下面return的部分就是基本的做法,當input的onChange觸發時會去打api
  return (
    <div>
      <input value={txt} onChange={txtChange} />
      {resData?.map(...)}
    </div>
  )
}

以範例為例我們來模擬一下當這個component被渲染的時候的先後順序,當畫面剛進入時,會以你帶入的預設值 txt = “” & resData = [] 去渲染,所以會是空的input和空白的結果。當我們於欄位中輸入 a 的時候會觸發 txtChange function 然後透過此 function 的處理程序會先觸發了 setTxt 的 function 去更改 txt 的值為 “a”,然後我們又將 txt 的值帶入 fetch function 裡面去打 api,並將回傳結果透過 setResData function 更改寫入 resData 中。但這個時候你應該會發現 resData 的值並沒有更動。

為什麼呢?讓我們重新看一遍這個 function 的處理順序:

// ...省略
const txtChange = (event) => {
  setTxt(event.target.value);
  // 這裡如果拿的是txt那麼會拿到原本就的值,其中的原理為js的閉包
  // 並不是非同步的處理問題,也就是說你應該要改的是將txt改為event.target.value
  // 因為透過setTxt的處理會經過一段Proxy的判斷處理程序
  // 而你所取出來的setTxt又是在useState的function當中,所以這裡仍然為閉包的概念
  console.log(txt) // 這裡你應該會拿到舊的值,也就是常常會讓人搞混的地方
  fetch(`/apiUrlsForSearch?${txt}`) // 解法就是將txt改為event.target.value
    .then((res) => res.json()).then(setResData)
}
// ...

這也是我真實經歷過的問題,面試官仍舊堅持自己的觀念為正確的,並確信 useState 為非同步的處理 function,老實說他一舉這樣的例子我當下也無法解釋的那麼清楚,我只記得我肯定沒在官方文件上看到說明 useState 為非同步的解釋,只能怪自己太菜了。

那麼其實更好一點的做法應該是將 fetch function 拆出來透過 useEffect 的方式去監聽原本的 txt 就好了:

const Search = () => {
  const [txt, setTxt] = useState('');
  const [resData, setResData] = useState([]);
  useEffect(() => {
    fetch(`/apiUrlsForSearch?${txt}`)
    .then((res) => res.json()).then(setResData)
  }, [txt])
  const txtChange = (event) => {
    setTxt(event.target.value);
  }

  return (
    <div>
      <input value={txt} onChange={txtChange} />
      {resData?.map(...)}
    </div>
  )
}

結語

這樣一來當你原本的 txt 更改的同時 useEffect 就會重複觸發 fetch function的部分,當然也可以透過 debounce 的做法來降低 server 的負擔。

那麼今天的分享就到這裡,明天我們再來詳解 useEffect

給全新手的大禮包

React基本Hook教學

參考資料

pattern電子書--proxy-pattern
solidjs Ryan Carniato直播


上一篇
React 走出新手村-回顧發展歷史
下一篇
React 走出新手村-深入useEffect
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
carternolan
iT邦新手 5 級 ‧ 2023-09-22 08:25:38

useState 的運作是同步還是非同步?

這段看了之後,覺得作者可能不太了解 JavaScript 的 event loop?
當某個 function 執行完之後,沒有拿到新的值,通常都是 async 的
如果是 sync 的,你的 setState 完成之後,正常來說應該要拿到新的值,但在這邊卻是沒有的
我也覺得你的面試官說得才是對的😆
同樣如果去 google 搜尋 useState sync or async 可能會查到滿多資料的

再來是 useState 的底層是 proxy, 想請問這邊的資料從哪裡來的呢?
proxy 是 Vue 底下資料綁定的實作,而 React 跟 Vue 在資料流處理方式也不一太一樣

LucianoLee iT邦研究生 5 級 ‧ 2023-09-22 10:09:17 檢舉

都2023年了,找文章正確性的能力如果不行,也可以問問chatGPT啊!😂😂😂
reddit react版
我看過的文章
Jack Herrington talking about useState
關於event loop你應該知道的💁🏼
也許我表達的太過主觀也沒有解釋得太清楚,我想要做的是結合上一篇的內容來實做一個類似的hook出來,這是寫文章時沒注意到沒看過上下文的用戶會搞混的地方。如果你有興趣了解當初怎麼寫出來的,你可以參考

如果你也有自己的想法歡迎show me the code,證明哪個環節是我missing掉的async。

React的 hook 讓你的 code 看起來是 sync 的
但是你的 setState 觸發之後的re-render流程卻不是在同一個 eventloop 內發生的
這樣的話感覺是對這塊的解釋手法有點問題,導致會讓人誤解

祝福作者的職涯順利囉( ^.< )

LucianoLee iT邦研究生 5 級 ‧ 2023-09-22 10:41:18 檢舉

其實我文中提到的閉包指的就是你上述的流程,很感謝你認真看完我的文章,也許我也可以參考你的說法,這樣會比較好讓不懂閉包的朋友理解我的意思。

也歡迎提出任何想法時提供連結,這樣我會比較好理解是不是我的資訊錯誤。

0
lijun
iT邦新手 4 級 ‧ 2023-10-19 10:50:44
LucianoLee iT邦研究生 5 級 ‧ 2023-10-19 11:04:52 檢舉

對請看清楚是 setState() ,如果對非同步有一定認知的話應該會理解他這裡講的Asynchronous和我們一般常講的promise,是不同的處理機制,有興趣可以參考我的medium文章,而我們使用的useState() 100% 是 sync 的機制,我文章裡面提到的問題就是閉包觀念的延伸題,有興趣理解更深的話可以補完這篇

0
lijun
iT邦新手 4 級 ‧ 2023-10-19 12:19:15

拍謝剛學React不久

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

  const increment = () => {
    // 延遲 2 秒後執行
    setTimeout(() => {
      // 更新狀態
      setCount(count + 1);
    }, 2000);
  };

  return (
    <div>
      <button onClick={increment}>+1</button>
      <p>count: {count}</p>
    </div>

如果是同步的話為什麼當我點擊下去後count的值沒有立刻增加
反而是等到setTimeout渲染頁面後才增加呢?

如果回調函數沒有副作用時就會是同步的

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

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <button onClick={increment}>+1</button>
      <p>count: {count}</p>
    </div>
  );
}
LucianoLee iT邦研究生 5 級 ‧ 2023-10-19 13:05:12 檢舉

沒關係你不孤單,我真的有遇過一個交大純血的資深前端面試官,問跟你一樣的問題,想當然直接被我扣分到爆,我建議你可以先看完下面幾篇關於 javascript 的基本觀念:
閉包問題
event loop講解
event loop深入理解

如果是同步的話為什麼當我點擊下去後count的值沒有立刻增加

這個問題就是沒有完全理解閉包和event loop的運作
還有先搞清楚 setTimeoutPromise 的差異,你可能會比較理解基本運作的原理。

這篇文章是不想離題太多,但我覺得你有必要理解 javascript 的運作原理,可以參考前面生成 v-dom 的概念延伸。
自製框架hook概念篇
Jack Herrington影片教學
實作派範例(請先把上面資源讀完)
學習資源與態度分享

lijun iT邦新手 4 級 ‧ 2023-10-19 18:15:58 檢舉

我查到的您看一下
React18 之後所有操作都是非同步處理的
React官方文件

解釋setState/useState執行的同步非同步問題

React核心成員的回復

腦要炸了QQ

LucianoLee iT邦研究生 5 級 ‧ 2023-10-19 20:56:45 檢舉

你才剛入坑,javascript的基礎不熟很正常,請先把我給你的那些資源念熟再往下吧!

function counterHook() {
    let count = 0;
    const add = (val: number) => count += val;
    return {count, add}
}

const count1 = counterHook();
count1.add(1);
console.log(count1.count);
count1.add(1);
console.log(count1.count);

能回答這題的執行結果嗎?
如果能,那怎麼改善錯誤呢?
錯誤的原因又是什麼呢?
如果你還不能理解,就代表你的閉包觀念根本沒弄清楚!請正視問題,資源我都分享給你了,那些是個人自律的部分。

關於React 18 batch updating的更動,也無法掩蓋個人閉包觀念不熟的部分,這裡可以給你學習資源自我進修。
Jack Herrington - Mastering batch updating

我要留言

立即登入留言