快速入門 React hooks + 後端集成

作者:LeanCloud 江宏

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 開發簡化了組件生命週期的概念,減少了代碼量,在開發者熟悉了這個新模式之後,還是一個很有價值的改變。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章