快速入門 React hooks + 後端整合

LeanCloud發表於2019-05-14

2019 年 2 月釋出的 React 16.8 正式引入了 hook 的功能。它使得 function 元件也像 class 元件一樣能維護狀態,所有的元件都可以寫成函式的形式,比起原有的以 class 的多個方法來維護元件生命週期的方式,簡化了程式碼,也基本消除了因為 this 繫結的問題造成的難以發現的 bug。這篇文章就介紹一下最常用的 state hook,以及在這種新的方式下怎麼與後端 API 通訊。

本文以一個管理任務的 Todo list 應用為例,可以增加新的任務,點選可以把任務標記為完成。部署好的效果可以在這裡看到,程式碼在這個 GitHub repo。這個 demo 使用 LeanCloud 作為儲存資料的後端,用的是一個 LeanCloud 開發版應用,所以可能遇到請求數超限的情況,建議在本地執行並替換進自己的 AppId 和 AppKey。

這個應用只有一個叫 App 的元件:

function App() {
  const [inputValue, setInputValue] = useState('');
  const [todos, setTodos] = useState(undefined);
  const [error, setError] = useState('');
複製程式碼

開頭先定義了它使用的狀態。useState的引數是狀態的初始值,它會返回一對結果:用來讀取這個狀態的一個只讀引用,以及一個設定狀態新值的函式。這裡建立了三個狀態: - inputValue: 輸入新任務的 <input> 元素的當前值 - todos: 當前顯示的任務。這裡初始值設為 undefined 表示尚未載入,而 [] 則意味著已經載入過,但是為空。 - error: 當前顯示的狀態資訊。

每次這個元件被重新渲染時,App() 這個函式都會被呼叫。每個 useState 只有第一次被呼叫時返回的狀態是初始值,之後每次都會返回已經記住的當前值。這裡有三個狀態,React 是用呼叫 useState 的順序來區分他們。可以理解為 App() 的所有狀態儲存在一個陣列裡,第一個 useState() 返回的是第一個狀態,第二個 useState() 返回的是第二個狀態,以此類推。所以使用 hook 必須保證這個元件函式每次執行中: 1. 對 useState() 的呼叫次數必須是一樣的。 2. 與各狀態對應的 useState()的呼叫順序是一樣的。

這就意味著 useState() 的呼叫不能放在條件分支或迴圈中。為了避免出錯,最好把所有 useState() 呼叫放在函式開頭。

接下來是新增一個任務的函式 addTodo

const addTodo = () => {
    saveTodo(inputValue).then(todo => {
      setInputValue('');
      setTodos(prev => [todo].concat(prev));
    }).catch(setError);
  };
複製程式碼

這裡 saveTodo() 是一個 helper 函式,會在文末介紹。在後端儲存了新任務後,會把輸入清空,並把新的任務加到用於顯示的任務列表的前面。這裡使用了設定新狀態的兩種方式:setInputValue('')直接設定新值,setTodos(prev => [todo].concat(prev)) 是傳遞一個更新狀態的函式。後者通常在新狀態依賴於舊狀態的時候使用。

再下一步檢查任務列表有沒有初始化過,如果沒有的話,就查詢後端資料把它初始化:

if (todos === undefined) {
    loadTodos().then(setTodos).catch(setError);
  }
複製程式碼

然後是定義如何切換任務的完成狀態:

const toggle = item => {
    item.set('finished', !item.get('finished'));
    item.save()
      .then(() => setTodos(prev => prev.slice(0)))
      .catch(setError);
  };
複製程式碼

這裡值得注意的是在設定 todos 的新值的時候用 prev.slice(0) 把這個陣列複製了一份。這是因為切換一個任務的狀態只是這個陣列中一個元素的一個屬性發生了改變。在使用 hook 更新狀態時,作為一個優化,React 會用 Object.is() 比較新老狀態,如果在這個語義下它們相等,React 會認為狀態沒有改變而不重新渲染這個元件。Object.is() 認為滿足以下條件之一的兩個值相等: - 兩個都是 undefined - 兩個都是 null - 兩個都是 true 或者都是 false - 兩個都是字串並且有相同的長度,相同的字元以相同的順序出現 - 兩個是同一個物件 - 兩個都是數字並且: - 都是 +0 - 都是 -0 - 都是 NaN - 都不是零或 NaN 並有相同的值。

這對於數字、布林、字串這樣 immutable 的簡單型別來說不是問題,但是對於陣列和物件來說,就意味著只有傳遞一個新的物件才會觸發渲染。好在這裡 slice(0) 只是做一個淺拷貝,沒有複製陣列引用的物件,所以代價是比較低的。

最後是把上面的一切放到渲染結果裡:

return (
    <div className={AppStyles.app}>
      <div className={AppStyles.error}>{error.toString()}</div>
      <div className={AppStyles.add}>
        <input placeholder="What to do next?" value={inputValue}
               onChange={e => setInputValue(e.target.value)}
               onKeyUp={e => { if (e.keyCode === 13) addTodo(); } } />
        <input type="button" value="↩" />
      </div>
      <ul>
        {todos && todos.map(item =>
                   <li key={item.getObjectId()}
                       onClick={() => toggle(item)}
                       data-finished={item.get('finished')}>
                     {item.get('content')}
                   </li> )}
      </ul>
    </div>
  );
}
複製程式碼

下面兩個函式是 App() 裡用到的從 LeanCloud 更新和載入資料的 saveTodo()loadTodos()

function saveTodo(content) {
  const Todo = LC.Object.extend('Todo');
  const todo = new Todo();
  todo.set('content', content);
  todo.set('finished', false);
  return todo.save();
}
​
function loadTodos() {
  const query = new LC.Query('Todo');
  query.equalTo('finished', false);
  query.limit(20);
  query.descending('createdAt');
  return query.find();
}
複製程式碼

有的人認為 React 的 hook 讓 React 變得更加「函式式」了。我的看法恰恰相反。把什麼都變成了 JavaScript 的 function 並不意味著程式更 functional 了。在有 hook 之前,React 的元件分為 class 元件和 function 元件,本來 function 元件可以看作是純函式,傳遞進去的 props 能決定渲染結果,是 functional 的。有了 hook 之後 function 也可以有狀態了,所以變成了披著 function 外衣的 object。如果不仔細瞭解實現機制的話,很容易產生一些微妙的 bug。不過也不可否認,使用 hook 開發簡化了元件生命週期的概念,減少了程式碼量,在開發者熟悉了這個新模式之後,還是一個很有價值的改變。

Photo by Chris Scott on Unsplash

相關文章