这篇文章主要介绍了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
React 约定,钩子一律使用use前缀命名,便于识别,这没什么可说的,要被特殊对待,就要服从一定的规则
Hooks作为钩子,存在与每个组件相关联的“存储器单元”的内部列表。 它们只是我们可以放置一些数据的JavaScript对象。 当你像使用useState()一样调用Hook时,它会读取当前单元格(或在第一次渲染时初始化它),然后将指针移动到下一个单元格。 这是多个useState()调用每个get独立本地状态的方式
解决为什么要使用hooks的问题,我决定从hooks解决了class组件的哪些痛点和hooks更符合react的组件模型两个方面讲述。
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或者定时器等那就更慌了~~
第二点:不同组件和生命周期方法之间的逻辑重复。 这个难度不亚于蜀道难——难于上青天!当然对于简单的逻辑可能通过HOC和render props来解决。但是这两种解决办法有两个比较致命的缺点,就是模式复杂和嵌套。
第三点:复杂的模式,比如render props和 HOC。 不得不说我在学习render props的时候不禁发问只有在render属性传入函数才是render props吗?好像我再任意属性(如children)传入函数也能实现一样的效果; 一开始使用HOC的时候打开React Develops Tools一看,Unknown是什么玩意~看着一层层的嵌套,我也是无能为力。
以上这三点都可以通过Hooks来解决(疯狂吹捧~)
我们知道,react强调单向数据流和数据驱动视图,说白了就是组件和自上而下的数据流可以帮助我们将UI分割,像搭积木一样实现页面UI。这里更加强调组合而不是嵌套,class并不能很完美地诠释这个模型,但是hooks配合函数式组件却可以!函数式组件的纯UI性配合Hooks提供的状态和副作用可以将组件隔离成逻辑可复用的独立单元,逻辑分明的积木他不香吗!
别问,问就是文档,如果不行的话,请熟读并背诵文档...
但是(万事万物最怕But), 既然是实践,就得假装实践过,下面就说说本人的简单实践和想法吧。
// 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) ... } 复制代码
// 这么实现很粗糙,可以配合useRef和useCallback,但即使这样也不完全等价于componentDidMount function useDidMount(handler){ React.useEffect(()=>{ handler && handler() }, []) } 复制代码
// 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都常量化)
// 实现计数功能 const [count, setCount] = React.useState(0); setCount(count => count + 1) // 展示用户信息 const initialUser = { name: 'Hello', age: '18', } const [user, setUser] = React.useState(initialUser) 复制代码
// 修改上面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,一篇支称整篇文章架构的深度好文!
/** 修改需求:每秒不是加多少可以由用户决定,可以看作不是+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]); ... } 复制代码
说这个之前,先说一说如果你要在FP里面使用函数,你要先要思考有替代方案吗?
方案1: 如果这个函数没有使用组件内的任何值,把它提到组件外面去定义
方案2:如果这个函数只是在某个effect里面用到,把它定义到effect里面
如果没有替代方案,就是useCallback出场的时候了。
// 场景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]); ... } 复制代码
// 存储不变的引用类型 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; } 复制代码
// 此栗子来自文档 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); 复制代码
// 此栗子来自文档 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> ); } 复制代码
说是彩蛋,其实是补充说明~~
hooks除了要以use开头,还有一条很很很很重要的规则,就是hooks只允许在react函数的顶层被调用(这里墙裂推荐Hooks必备神器eslint-plugin-react-hooks)
考虑到出于研(gang)究(jing)精神的你可能会问,为什么不能这么用,我偏要的话呢?如果我是hooks开发者,我会毫不犹豫地说出门右转,有请下一位开发者!当然如果你想知道为什么这么约定地话,还是值得探讨一下的。其实这个规则就是保证了组件内的所有hooks可以按照顺序被调用。那么为什么顺序这么重要呢,不可以给每一个hooks加一个唯一的标识,这样不就可以为所欲为了吗?我以前一直都这么想过直到Dan给了我答案,简单点说就是为了hooks最大的闪光点——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 }; } 复制代码
最后抛出两个讨论的小问题。
React Hooks没有缺点吗?
- 肯定是有的,给我最直观的感受就是令人又爱又恨的闭包
- 不断地重复渲染会带来一定的性能问题,需要人为的优化
上面说了写了很多的setInterval的代码,可以考虑封装成一个custom-hooks?
- 可以考虑封装成useInterva,关于封装还是墙裂推荐Dan的 Making setInterval Declarative with React Hooks
- 如果有一堆特定的功能hooks,是不是完全可以通过组装各种hooks完成业务逻辑的开发,例如网络请求、绑定事件监听等
本人能力有限,如果有哪里说得不对的地方,欢迎批评指正!
真的真的最后,怕你错过,再次安利Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!