快速入门 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 开发简化了组件生命周期的概念,减少了代码量,在开发者熟悉了这个新模式之后,还是一个很有价值的改变。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容