最近写react 函数式组件感觉好爽。
react提供两种写组件的方式,任何一个组件,可以用类来写,也可以用函数来写。下面是类的写法。
class Home extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
再来看钩子的写法,也就是函数。
const Home = (props) => {
return <h1>Hello, {props.name}</h1>;
}
这两种写法,作用完全一样。那么:"我应该使用哪一种写法?"
官方推荐使用函数,而不是类。因为函数更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,函数,更符合 React 函数式的本质。
下面是类组件和函数组件代码量的比较。对于复杂的组件,可能差的就更多了。
class Home1 extends React.Component {
state = {
name: "xxx",
};
componentDidMount() {
this.setState({
name: "小明",
});
}
render() {
return <h1>Hello, {this.state.name}</h1>;
}
}
const Home2 = () => {
const [name, setName] = useState("xxx");
useEffect(() => {
setName("小明");
}, []);
return <h1>Hello, {name}</h1>;
};
-
基础 Hook
- useState
- useEffect
- useContext
1、组件的分类
一般我们写组件分为容器组件和展示组件。
- 容器组件:包含了数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。
- 展示组件: 只应该做一件事,就是展示。 同时也是个 纯函数。
这个函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 "纯函数"
const Home = (props) => {
return <h1>Hello, {props.name}</h1>;
}
2、什么是 Hook?
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
之前在 React 组件中执行过数据获取、订阅或者手动修改过 DOM、状态更新。我们统一把这些操作称为 副作用,或者简称为“作用”。
3、什么是 副作用?
看到这里,你可能会产生一个疑问:如果纯函数只能进行数据计算,那些不涉及计算的操作(比如生成日志、储存数据、改变应用状态等等)应该写在哪里呢?
函数式编程将那些跟数据计算无关的操作,都称为 "副作用"。如果函数内部直接包含产生副作用的操作,就不再是纯函数了,我们称之为不纯的函数。
纯函数内部只有通过间接的手段(这手段就是hook),才能包含副效应。
4、hook 的作用
hook 就是 React 函数组件的副作用解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副作用)都必须通过钩子引入。
5、什么时候我会用 Hook?
如果你在编写函数组件并意识到需要向其添加一些 副作用,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
6、useState Hook 声明 State 变量
声明一个 name 的变量
const Home = () => {
const [name, setName] = useState("xxx");
return <h1>Hello, {name}</h1>;
};
参数
useState() 方法里面唯一的参数就是初始 state。 它可以是任意类型的值(any)。
注意:如果我们想要在 state 中存储两个不同的变量,只需调用
useState()两次即可。
返回值
当前 state 以及更新 state 的函数。这就是我们写 const [name, setName] = useState("xxx") 的原因。这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。
方括号为
数组结构, [name, setName] = useState("xxx") => [a, b] = [1, 2]
读取 State
在函数中,我们可以直接用 name:
<p>Your Name, {name}</p>
更新 State
在函数中,我们已经有了 setName 和 name 变量,所以我们不需要 this:
<button onClick={() => setName('小明')}> Click me
</button>
函数式更新(常用)
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
const Counter = () => {
const [count, setCount] = useState(0);
return (
<>
Count: {count}
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
总结
现在让我们来仔细回顾一下学到的知识,看下我们是否真正理解了。
1: import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(0);
5:
6: return (
7: <div>
8: <p>You clicked {count} times</p>
9: <button onClick={() => setCount(state => state + 1)}>10: Click me
11: </button>
12: </div>
13: );
14: }
- 第一行: 引入 React 中的
useStateHook。它让我们在函数组件中存储内部 state。 - 第四行: 在
Example组件内部,我们通过调用useStateHook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为count,因为它存储的是点击次数。我们通过传0作为useState唯一的参数来将其初始化为0。第二个返回的值本身就是一个函数。它让我们可以更新count的值,所以我们叫它setCount。 - 第九行: 当用户点击按钮后,我们传递一个新的值给
setCount。React 会重新渲染Example组件,并把最新的count传给它。
思考,对比calss一些操作误区
当我们的state是一个对象修改某属性时候。
class:
class App extends React.Component {
state = {
name: "xxx",
age: 18,
};
componentDidMount() {
this.setState({
name: "小明",
});
}
render() {
return (
<>
<div>名字:{this.state.name}</div>
<div>年龄:{this.state.age}</div>
</>
);
}
}
你可能需要这样子做,函数组件:
const Home = () => {
const [user, setName] = useState({
name: "xxx",
age: 18,
});
useEffect(() => {
// setName({ name: "小明" }); // 错误写法
setName((state) => ({ ...state, name: "小明" }));
}, []);
return (
<>
<div>名字:{user.name}</div>
<div>年龄:{user.age}</div>
</>
);
};
数据更新完成做些操作
calss:
this.setState(
{
name: "小明",
},
() => {
console.log(this.state.name);
// 获取最新数据、接口请求等
}
);
你可能需要这样子做,函数组件:
useEffect(() => {
setName((state) => ({ ...state, name: "小明" }));
}, []);
useEffect(() => {
console.log(user.name);
// 获取最新数据、接口请求等
}, [user.name]);
当非[user.name]改变时不会触发useEffect,如:setName((state) => ({ ...state, age: 20 }))
7、useEffect Hook 副作用操作
Effect Hook 可以让你在函数组件中执行副作用操作
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。
useEffect() 的用法
useEffect()本身是一个函数,由 React 框架提供,在函数组件内部调用即可。
举例来说,我们希望组件加载以后,网页标题(document.title)会随之改变。那么,改变网页标题这个操作,就是组件的副作用,必须通过useEffect()来实现。
import React, { useEffect } from 'react';
function Welcome(props) {
useEffect(() => {
document.title = '加载完成';
});
return <h1>Hello, {props.name}</h1>;
}
上面例子中,useEffect()的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。
useEffect()的作用就是指定一个副作用函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。
useEffect() 的第二个参数
有时候,我们不希望useEffect()每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副作用函数的依赖项,只有依赖项发生变化,才会重新渲染。
function Welcome(props) {
useEffect(() => {
document.title = `Hello, ${props.name}`;
}, [props.name]);
return <h1>Hello, {props.name}</h1>;
}
上面例子中,useEffect()的第二个参数是一个数组,指定了第一个参数的依赖项(props.name)。只有该变量发生变化时,副效应函数才会执行。
如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于不依赖任何变量,所以那些变量无论怎么变,函数的执行结果都不会改变,所以运行一次就够了。
useEffect() 的返回值
副作用是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副作用。
useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副作用。如果不需要清理副作用,useEffect()就不用返回任何值。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);
上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。
实际使用中,由于副作用函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副作用函数重新执行之前,也会执行一次,用来清理上一次渲染的副作用。
虚拟生命周期
你可以用 useEffect Hook给函数组件实现三个生命周期:
componentDidMount``componentDidUpdate 和 componentWillUnmount
componentDidMount
useEffect(() => {
// 只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。
}, []);
componentDidUpdate
useEffect(() => {
// 指定了一个参数的依赖项(name)。只有该变量发生变化时,副效应函数才会执行。
}, [name]);
componentWillUnmount
useEffect(() => {
// `useEffect()`允许返回一个函数,在组件卸载时,执行该函数,清理副作用。
const timeout = setTimeout(() => {
alert("Hello");
}, 3000);
return () => {
clearTimeout(timeout);
}
}, []);
8、useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
父组件:
import Toolbar from "./Toolbar";
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
export const ThemeContext = React.createContext(themes.light);
const App = () => {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
export default App;
某子组件:
import ThemeContext from "./index";
export const ThemedButton = () => {
// 接收一个 context 对象(`React.createContext` 的返回值)
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。