快速入門 React hooks + 後端集成

2019 年 2 月發佈的 React 16.8 正式引入了 hook 的功能。它使得 function 組件也像 class 組件同樣能維護狀態,全部的組件均可以寫成函數的形式,比起原有的以 class 的多個方法來維護組件生命週期的方式,簡化了代碼,也基本消除了由於 this 綁定的問題形成的難以發現的 bug。這篇文章就介紹一下最經常使用的 state hook,以及在這種新的方式下怎麼與後端 API 通信。javascript

本文以一個管理任務的 Todo list 應用爲例,能夠增長新的任務,點擊能夠把任務標記爲完成。部署好的效果能夠在這裏看到,代碼在這個 GitHub repo。這個 demo 使用 LeanCloud 做爲存儲數據的後端,用的是一個 LeanCloud 開發版應用,因此可能遇到請求數超限的狀況,建議在本地運行並替換進本身的 AppId 和 AppKey。java

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

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

開頭先定義了它使用的狀態。useState的參數是狀態的初始值,它會返回一對結果:用來讀取這個狀態的一個只讀引用,以及一個設置狀態新值的函數。這裏建立了三個狀態: - inputValue: 輸入新任務的 <input> 元素的當前值 - todos: 當前顯示的任務。這裏初始值設爲 undefined 表示還沒有加載,而 [] 則意味着已經加載過,可是爲空。 - error: 當前顯示的狀態信息。git

每次這個組件被從新渲染時,App() 這個函數都會被調用。每一個 useState 只有第一次被調用時返回的狀態是初始值,以後每次都會返回已經記住的當前值。這裏有三個狀態,React 是用調用 useState 的順序來區分他們。能夠理解爲 App() 的全部狀態存儲在一個數組裏,第一個 useState() 返回的是第一個狀態,第二個 useState() 返回的是第二個狀態,以此類推。因此使用 hook 必須保證這個組件函數每次運行中: 1. 對 useState() 的調用次數必須是同樣的。 2. 與各狀態對應的 useState()的調用順序是同樣的。github

這就意味着 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)) 是傳遞一個更新狀態的函數。後者一般在新狀態依賴於舊狀態的時候使用。bash

再下一步檢查任務列表有沒有初始化過,若是沒有的話,就查詢後端數據把它初始化:app

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

相關文章
相關標籤/搜索