阅读 805

React Hooks:初探·实践

前言

这篇文章主要介绍了React Hooks的一些实践用法和场景,遵循我个人一贯的思(tao)路(是什么-为什么-怎么做)

是什么

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

简单来说,上面这段官腔大概翻(xia)译(shuo)就是告诉我们class能够做到的老子用hooks基本可以做到,放弃抵抗吧,少年!

其实按照我自己的看法:React Hooks是在函数式组件中的一类以use为开头命名的函数。 这类函数在React内部会被特殊对待,所以也称为钩子函数。

  • 函数式组件

Hooks只能用于Function Component, 其实这么说不严谨,我更喜欢的说法是建议只在于Function Component使用Hooks

Should I use Hooks, classes, or a mix of both?
excuse me?

  • use开头

React 约定,钩子一律使用use前缀命名,便于识别,这没什么可说的,要被特殊对待,就要服从一定的规则

  • 特殊对待

Hooks作为钩子,存在与每个组件相关联的“存储器单元”的内部列表。 它们只是我们可以放置一些数据的JavaScript对象。 当你像使用useState()一样调用Hook时,它会读取当前单元格(或在第一次渲染时初始化它),然后将指针移动到下一个单元格。 这是多个useState()调用每个get独立本地状态的方式

为什么

解决为什么要使用hooks的问题,我决定从hooks解决了class组件的哪些痛点和hooks更符合react的组件模型两个方面讲述。

1. class组件不香吗?

class组件它香,但是暴露的问题也不少。Redux 的作者 Dan Abramov总结了几个痛点:

  • Huge components that are hard to refactor and test.
  • Duplicated logic between different components and lifecycle methods.
  • Complex patterns like render props and higher-order components.

第一点:难以重构和测试的巨大组件。 如果让你在一个代码行数300+的组件里加一个新功能,你不慌吗?你尝试过注释一行代码,结果就跑不了或者逻辑错乱吗?如果需要引入redux或者定时器等那就更慌了~~

第二点:不同组件和生命周期方法之间的逻辑重复。 这个难度不亚于蜀道难——难于上青天!当然对于简单的逻辑可能通过HOCrender props来解决。但是这两种解决办法有两个比较致命的缺点,就是模式复杂和嵌套。

第三点:复杂的模式,比如render props和 HOC。 不得不说我在学习render props的时候不禁发问只有在render属性传入函数才是render props吗?好像我再任意属性(如children)传入函数也能实现一样的效果; 一开始使用HOC的时候打开React Develops Tools一看,Unknown是什么玩意~看着一层层的嵌套,我也是无能为力。

以上这三点都可以通过Hooks来解决(疯狂吹捧~)

2. hooks更符合React的编程模型

我们知道,react强调单向数据流和数据驱动视图,说白了就是组件和自上而下的数据流可以帮助我们将UI分割,像搭积木一样实现页面UI。这里更加强调组合而不是嵌套,class并不能很完美地诠释这个模型,但是hooks配合函数式组件却可以!函数式组件的纯UI性配合Hooks提供的状态和副作用可以将组件隔离成逻辑可复用的独立单元,逻辑分明的积木他不香吗!

真香

怎么做

别问,问就是文档,如果不行的话,请熟读并背诵文档...

但是(万事万物最怕But), 既然是实践,就得假装实践过,下面就说说本人的简单实践和想法吧。

1. 转变心智模型

jQuery
我认为学习Hooks的主要成本不在于api的学习,而是在于心智模型的转变~就像是当年react刚出时,jQuery盛行的时代,这也需要时间去理解这种基于virtual DOM的心智模型。出于本能,我们总喜欢在新事物的身上寻找旧事物的共同点,这种惯性思维应该批判性地对待(上升到哲学层面了,赶紧回归正题....),如果你在学习的过程中也有过把class组件的那套搬到Hooks,那么恭喜你,你可能会陷入无限的wtf····, 下面举几个例子

  1. state一把梭
// in class component
class Demo extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     name: 'Hello',
     age: '18',
     rest: {},
   }
 }
 ...
}

// in function component
function Demo(props) {
 const initialState = {
   name: 'Hello',
   age: '18',
   rest: {},
 }
 const [state, setState] = React.useState(initialState)
 ...
}
复制代码
  1. 尝试模拟生命周期
// 这么实现很粗糙,可以配合useRef和useCallback,但即使这样也不完全等价于componentDidMount
function useDidMount(handler){
  React.useEffect(()=>{
      handler && handler()
  }, [])
}
复制代码
  1. 在useEffect使用setInterval有时会事与愿违
// count更新到1就不动了
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  ...
}
复制代码

其实,在class component环境下思考问题更像是在特定的时间点做特定的事情,例如我们会在constructor中初始化state,会在组件挂载后(DidMount)请求数据等,会在组件更新后(DidUpdate)处理状态变化的逻辑,会在组件卸载前(willUnmount)清除一些副作用

然而在hooks+function component环境下思考问题应该更趋向于特定的功能逻辑,以功能为一个单元去思考问题会有一种豁然开朗的感觉。例如改变document的title、网络请求、定时器... 对于hooks,只是为了实现特定功能的工具而已

你会发现大部分你想实现的特定功能都是有副作用(effect)的,可以负责任的说useEffect是最干扰你心智模型的Hooks, 他的心智模型更接近于实现状态同步,而不是响应生命周期事件。还有一个可能会影响你的就是每一次渲染都有它自己的资源,具体表现为以下几点

  • 每一次渲染都有它自己的Props 和 State:当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的状态值,这个状态值是函数中的一个常量(也就是会说,在任意一次渲染中,props和state是始终保持不变的)
  • 每一次渲染都有它自己的事件处理函数:和props和state一样,它们都属于一次特定的渲染,即便是异步处理函数也只能拿到那一次特定渲染的状态值
  • 每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state(建议在分析问题时,将每次的渲染的props和state都常量化)

2. 所谓Hooks实践

useState —— 相关的状态放一起

  • 不要所有state一把梭,可以写多个useState,基本原则是相关的状态放一起
  • setXX的时候建议使用回调的形式setXXX(xxx => xxx...)
  • 管理复杂的状态可以考虑使用useReducer(如状态更新依赖于另一个状态的值)
// 实现计数功能
 const [count, setCount] = React.useState(0);
 setCount(count => count + 1)
 
// 展示用户信息
const initialUser = {
  name: 'Hello',
  age: '18',
}
const [user, setUser] = React.useState(initialUser)
复制代码

useEffect —— 不接受欺骗的副作用

  • 不要对依赖数组撒谎,effect中用到的所有组件内的值都要包含在依赖中。这包括props,state,函数等组件内的任何东西
  • 不要滥用依赖数组项, 让Effect自给自足
  • 通过返回一个函数来清除副作用,在重新渲染后才会清除上一次的effects
// 修改上面count更新到1就不动了,方法1
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);
  ...
}
// 修改上面count更新到1就不动了,方法2( 与方法1的区别在哪里 )
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  ...
}
复制代码

关于useEffect, 墙裂推荐Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!

useReducer —— 强大的状态管理机制

  • 把组件内发生了什么(actions)和状态如何响应并更新分开表述,是Hooks的作弊模式
/** 修改需求:每秒不是加多少可以由用户决定,可以看作不是+1,而是+step*/

// 方法1
function Counter() {
  const [count, setCount] = React.useState(0);
  const [step, setStep] = React.useState(1);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);
  ...
}
// 方法2( 与方法1的区别在哪里 )
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { ...state, count: count + step };
  } else if (action.type === 'step') {
    return { ...state, step: action.step };
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  ...
}
复制代码

useCallback —— FP里使用函数的好搭档

说这个之前,先说一说如果你要在FP里面使用函数,你要先要思考有替代方案吗?

方案1: 如果这个函数没有使用组件内的任何值,把它提到组件外面去定义

方案2:如果这个函数只是在某个effect里面用到,把它定义到effect里面

如果没有替代方案,就是useCallback出场的时候了。

  • 返回一个 memoized 回调, 不要对依赖数组撒谎
// 场景1:依赖组件的query
function Search() {
  const [query, setQuery] = React.useState('hello');
  
  const getFetchUrl = React.useCallback(() => {
    return `xxxx?query=${query}`;
  }, [query]);  

  useEffect(() => {
    const url = getFetchUrl();
  }, [getFetchUrl]); 
  ...
}

// 场景2:作为props
function Search() {
   const [query, setQuery] = React.useState('hello');

  const getFetchUrl = React.useCallback(() => {
    return `xxxx?query=${query}`;
  }, [query]);  

  return <MySearch getFetchUrl={getFetchUrl} />
}

function MySearch({ getFetchUrl }) {
  useEffect(() => {
    const url = getFetchUrl();
  }, [getFetchUrl]); 
  ...
}
复制代码

useRef —— 有记忆功能的可变容器

  • 返回一个可变的 ref 容器对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变,也就是说会在每次渲染时返回同一个 ref 对象
  • 当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染
  • 可以在ref.current 属性中保存一个可变值的“盒子“。常见使用场景:存储指向真实DOM / 存储事件监听的句柄 / 记录Function Component在某次渲染的值( eg:上一次state/props,定时器id.... )
// 存储不变的引用类型
const { current: stableArray } = React.useRef( [1, 2, 3] )
<Comp arr={stableArray} />

// 存储dom引用
const inputEl = useRef(null);
<input ref={inputEl} type="text" />

// 存储函数回调
const savedCallback = useRef();
useEffect(() => {
    savedCallback.current = callback;
}
复制代码

useMemo —— 记录开销大的值

// 此栗子来自文档
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码

useContext —— 功能强大的上下文

  • 接收一个 context (React.createContext 的返回值)并返回该 context 的当前值,当前的 context 值由上层组件中最先渲染的 <MyContext.Provider value={value}> 的 value决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值,如果重新呈现组件非常昂贵,那么可以通过使用useMemo来优化它
// 此栗子来自文档
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}
复制代码

彩蛋

说是彩蛋,其实是补充说明~~

1. 一条重要的规则(代码不规范,亲人两行泪)

hooks除了要以use开头,还有一条很很很很重要的规则,就是hooks只允许在react函数的顶层被调用(这里墙裂推荐Hooks必备神器eslint-plugin-react-hooks)

考虑到出于研(gang)究(jing)精神的你可能会问,为什么不能这么用,我偏要的话呢?如果我是hooks开发者,我会毫不犹豫地说出门右转,有请下一位开发者!当然如果你想知道为什么这么约定地话,还是值得探讨一下的。其实这个规则就是保证了组件内的所有hooks可以按照顺序被调用。那么为什么顺序这么重要呢,不可以给每一个hooks加一个唯一的标识,这样不就可以为所欲为了吗?我以前一直都这么想过直到Dan给了我答案,简单点说就是为了hooks最大的闪光点——custom-hooks

2. custom-hooks

给我的感觉就是custom-hooks是一个真正诠释了React的编程模型的组合的魅力。你可以不看好它,但它确实有过人之处,至少它呈现出思想让我越想越上头~~以至于vue3.0也借鉴了他的经验,推出了Vue Hooks。反手推荐一下react conf 2018的custom-hooks。

魅力

// 修改页面标题
function useDocumentTitle(title) {
  useEffect (() => {
    document.title = title;
  }, [title]);
}

// 使用表单的input
function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}
复制代码

写在最后

最后抛出两个讨论的小问题。

  1. React Hooks没有缺点吗?

    • 肯定是有的,给我最直观的感受就是令人又爱又恨的闭包
    • 不断地重复渲染会带来一定的性能问题,需要人为的优化
  2. 上面说了写了很多的setInterval的代码,可以考虑封装成一个custom-hooks?

    • 可以考虑封装成useInterva,关于封装还是墙裂推荐Dan的 Making setInterval Declarative with React Hooks
    • 如果有一堆特定的功能hooks,是不是完全可以通过组装各种hooks完成业务逻辑的开发,例如网络请求、绑定事件监听等

本人能力有限,如果有哪里说得不对的地方,欢迎批评指正!

对不听系列

真的真的最后,怕你错过,再次安利Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!

关注下面的标签,发现更多相似文章
评论