hook 大全
hook 简介
hooks 是啥
hooks 是 react 在 16.8 版本引入的新的特性,可以让我们在不编写类的情况下能够使用 state 以及其他的新的特性。
为什么要用 hook
一、不知道大家有没有写类组件有没有被生命周期函数的名字恶心到,如果没被恶心到的肯定是习惯了,一开始学习 react 的时候看到 componentWillReceiveProps(已经废弃)、componentwillMount、getDerviesStateFromProps 这样长的不得了的生命周期是真的烦,因为需要看很多次才能记住。还有就是这么多钩子以及随着新版本的迭代一直不断在新增与替换,需要不断关注他们的变化。
二、用类组件写代码的时候,我们需要在 componentDidMount 上去订阅一些事件,在 componentWillUnMount 上去取消订阅,按照常理来说的话最好是按照逻辑关系去写到一块,但是由于生命周期的缘故不得不拆开写,所以说造成了具有相同逻辑的代码放在不同的地方,一旦 componentDidMount 有几百行代码,迭代的话谁也不敢动,祖传代码有很多坑。造成迭代的时候越写越多,最终导致代码不可维护。
三、代码复用、相同逻辑代码在类组件复用的话是通过高阶组件的方式即传递进去一个组件,给这个组件加工返回一个新的组件。这个代码的话写起来很麻烦的。另外一种处理方式是 reder Props 也可以处理。另外是混入(但是 react 已经废弃掉了)。这三种情况我觉得不如自定义 hook 来的方便。毕竟函数复用好复用,类复用的话会比较复杂。
四、另外由于是类组件使得我们不得不关注 this 指向的问题,因为稍不留意就会报 undefined 的错误。
五、hooks 为我们解决了以上难题,useState+useEffect 可以完美解决第一个与第二个问题。自定义 hook 为我们解决了第三个问题。所以说为了我们写代码更高效,为了能够多划水尽量用 hook。
介绍第一个 hook
useState
虽然上面我一直在吐槽 react 生命周期,但是讲 hook 的话我得依赖于声明周期去讲,不然的话就不知道怎么讲了,该学的类组件还是要学的,总归逃不掉的。
useState 在什么时间点去执行
1、在初始化的时候去执行 useState 它的执行时机相当于类组件的 constructor 执行时机是一样的
举个栗子
import React, { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>点我加一</button>
</>
);
};
export default App;
解释
1、首先的话这个代码useState()先执行,useState返回值为数组其第一个值为数据第二个值为更改数据的函数,我们可以通过数组解构的方式去拿到这个数据count,与改变这个数据的函数setCount
2、其中count与setCount可以随意命名但是一般建议命名为有意义的名字。
3、另外需要注意的是useState()的参数只执行一次,而且参数可以为函数但是函数中必须要返回一个数据类型比如 function() {return 0;}这是返回的基本数据型,这个与useState(0)无任何差别。但是这个函数只执行一次。
4、看看它是如何执行的当点击setCount的时候会拿到当前的count值+1然后重新渲染,在点击的话setcount又会拿到当前的值然后将count的值+1再重新渲染所以的话随着我们点击count为这样便0、1、2、3、4 ······这样的
useState()的参数在初始化渲染的时候只执行一次在上一个函数表现为当页面渲染的时候的初始值为0,然后这个count不一定像类组件的state一定为一个
对象、count可以为基本数据类型、只要useState传递什么值count就是什么值
5、另外hook的好处在这里可以体现出来比如说是hook不用管this的指向问题了
。
6、另外如果我们想再定义一个visibile变量只需要加上const [visibile, setVisibile] = useState(false) 即可,将useState返回的数据解构出来为visibile与
setVisibile的话就可以使用了,当然如果你愿意useState的参数可以传递对象。
7、与类组件的setState不同的是这个hooks中的setState不会自动合并对象,hook需要我们手动合并对象。但是类组件的setState会帮我们合并对象
栗子如下:
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "隔壁老王",
age: 2,
};
}
handleClick = () => {
this.setState({ name: "老王" });
};
render() {
return (
<>
{this.state.name}
{this.state.age}
<button
onClick={() => {
this.handleClick();
}}
>
点击改变名字
</button>
</>
);
}
}
现象与原因 当我们点击按钮更改名字的时候name发生了变化但是age是不会改变的,因为这里我们并没有修改age值,其实深层次的原因是类组件将新的数组与旧的数据进行合并了 再举一个栗子
const App = () => {
const [state, setState] = useState({ name: "隔壁老王", age: 1 });
const handleClick = () => {
setState({ name: "老王" });
};
return (
<>
{state.name}
{state.age}
<button onClick={() => handleClick()}>点击改变名字</button>
</>
);
};
export default App;
现象与原因 在这个栗子中当点击按钮的后age没有了,是因为{name:"老王"}将数据替换掉了,而不是将数据合并,所以这一点与类组件不同,使用的时候要特别注意。
useEffect
import React, { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `${count}`;
});
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>点我加一</button>
</>
);
};
export default App;
注意点
注意哈
1、先说一下useEffect是在什么时候执行的吧,在render函数执行之后执行了,也就是在上面这个栗子中的return后执行了。这个就是useEffect执行时机。
2、说一下这个代码的意思是useState这个就不用说了,useState在上面讲过了,在return里面的数据执行之后useEffect去执行,所以我们可以看到document.title为0,然后当我们点击的时候重新渲染然后count变为1,然后return里面的函数执行之后再执行useEffect这个时候可以看到useEffect变为1,所以依次类推
3、useEffect()有两个参数,上面的例子是传递的第一个参数为函数第二个参数没有传,这就意味着每次执行就相当于类组件的componentDidMount+componentDidUpdated这两个生命周期。
4、当我们想要类组件的componentDidMount的生命周期的时候我们可以这样写,传入第二个参数,而第二个参数第意思若第二个参数发生变化useEffect就执行,所以嘛,传递一个空数组,空数组永远不发生变化则useEffect只执行一次,这就相当于componentDidMount这个生命周期,栗子如下
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `${count}`;
}, []);
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>点我加一</button>
</>
);
};
export default App;
解释
这个栗子与上面的栗子的区别仅仅在useEffect的第二个参数加了一个空数组然后因为这个空数组不依赖于任何变量所以说这里的useEffect只执行一次
5、话又说回来了如果useEffect的第二个参数传入count的话的表象是什么,这个其实很简单,与useEffect不传入第二个参数一样,因为一旦第二个参数传入为count的话第一次渲染了相当于componentDidMount的生命周期,但是当我们点击button按钮的话count的值发生改变,useEffect检测到值发生了改变所以说就会再次执行。由于App函数中只有count一个变量,不考虑props的情况下,重新渲染的话只能修改count,而useEffect又是依赖于count也就是它传递的第二个参数是count所以就相当于componentDidUpdated这个生命周期。
6、如何把相同的逻辑写在同一个函数内呢
栗子
useEffect(() => {
const add = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(add);
};
}, []);
解释
只需要将useEffect的第一个参数写上return函数就行,这个useEffect会在函数卸载的时候执行,所以可以处理我们的一些绑定的事件等等操作。这样就可以把相同的逻辑写在同一个函数之中了。
7、栗子
useEffect(() => {
const add = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(add);
};
});
解释
这个函数在什么时候执行clearInterval呢,是在重新setCount执行完之后就执行return里面的函数清除定时器,也就说在这个状态下的定时器不会遗留到下一个状态
8、栗子
const App = () => {
const [state, setState] = useState({ name: "隔壁老王", age: 1 });
useEffect(() => {
const add = setInterval(() => {
console.log(state);
}, 1000);
return () => {
clearInterval(add);
};
}, [state.age]);
const handleClick = () => {
setState({
...state,
name: "12",
});
};
return (
<>
{state.name}
{state.age}
<button
onClick={() => {
handleClick();
}}
>
点击我重新渲染
</button>
</>
);
};
export default App;
解释
当我们点击按钮的时候命名name变化了单数useEffect打印的name还是原来的,这是因为useEffect是依赖于age来重新执行的,因为age没有发生变化,所以useEffect不会重新执行,就造成了useEffect里面的值是它执行拿到的值。后来name变化了,useeffect不会重新执行所以说useEffect还是一开始拿到的值
9、栗子
useEffect(() => {
const add = setInterval(() => {
setState({
...state,
age: state.age + 1,
});
}, 1000);
return () => {
clearInterval(add);
};
}, []);
这个的话 age 变化为 1 就不会再次发生变化了,原因同上一个栗子
注意
解释 useEffect中第一个参数用到什么变量,第二个参数的要传递相应的变量,否则会出现一些意想不到的bug 好了,会这两个hook基本上可以做日常80%的开发了。
useContext
我们讲一下useContext
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.light}>
<Toolbar />
<button onClick={() => {}}></button>
</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>
);
}
诺这就是这个其实和Consumer差不多
useReducer
大家在用redux的时候是不是经常写一些dispatch与reducer的什么的嘛,useReducer的话与redux类似
举个栗子
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
export default Counter;
解释 使用useReducer传入reducer与初始化数据initialState这个与redux差不多,返回的是一个数组,机构出来的话一个是state,另一个是dispatch,大家都对这很熟悉了我就不再说了。 所以,当我们dispatch派发一个type的话reducer根据type的不同执行不同的代码,将处理之后的数据返回。 这个useReducer可以结合Context来在一些简单的项目中去替代reudx,所以以后项目开发的时候不妨考虑考虑useReducer+Context这种方式
useCallback
栗子
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
解释 useCallback的第二个参数是根据这个来执行useCallback的,如果a或者b发生变化则useCallback就会再次执行,不发生变化的话useCallback只执行一次。 useCallback返回一个返回一个 memoized 回调函数。我们可以用memo与useCallback与useMemo来执行性能优化,所以说我们可以将开销比较大的函数放在useCallback中让它在条件满足的时候重新执行,不满足则不执行
useMemo
栗子
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
这与useCallback是差不多的但是useCallback的话返回memoized函数,而memoized返回的是值
useRef
栗子
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
解释 类组件的createRef与useRef()是十分相似的,所以我们通过inputE1.current可以拿到原生的dom,由于inputEl在渲染的时候对象地址保持不变所以的话,这个current我们可以存储某一时间的值。
目前常用的hook在这里了,我以后会随时补充,如果各位观众老爷不介意的话麻烦点一个赞