玩转React基础 Hook

980 阅读7分钟

最近写react 函数式组件感觉好爽。

20e885f15989f84dfc58e9948f42fec6.jpeg

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 中的 useState Hook。它让我们在函数组件中存储内部 state。
  • 第四行:  在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 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>
  );
}