从零开始的野路子React/Node(5)近期Hooks使用体会

上周实习生大佬休假,导致疯狂赶工,在一如既往的复制-黏贴-修改中,竟也渐渐琢磨出一点前端的感觉来。这一期主要讲讲最近使用Hooks的心得。

(以下梗皆出自B站最近挺火的《啊!海军》)

1、useState 监听自身的改变

useState可以视作专门监听某一个变量的改变,当其发生变化时,重新渲染页面。

useState监听的这个变量不仅仅可以是简单类型(比如整数,字符串……)也可以是一个Object。这也就意味着其实useState可以同时监听多个值,比如(新建一个UpAndDown组件,再把它放入App.js中):

import React, {useState} from 'react';

export default function UpAndDown() {
    const [comment, setComment] = useState({"up":0, "down":0});

    const handleUp = () => {
        setComment({...comment, up:comment.up+1})
    };

    const handleDown = () => {
        setComment({...comment, down:comment.down+1})
    };

    return (
        <>
            <p>{ comment.up }</p>
            <button onClick={ handleUp }>+很有精神</button>
            <p>{ comment.down }</p>
            <button onClick={ handleDown }>-听不见</button>
        </>
    )
};

我们利用了同一个useState来分别监听up和down两个值得变化,任意一个发生改变时(点击“+很有精神”或者“-听不见”),都会触发重渲染来更新页面(两者分开计数,互不影响)。

 

2、useEffect 加载与被动改变

useEffect只在两种时候执行内部的内容,从而触发重渲染。一是组件加载的时候,另一个是[]参数中的内容更新的时候(被动更新)。

比如以下这段(新建一个AddOn组件,再把它放入App.js中):

import React, {useState, useEffect} from 'react';

export default function AddOn() {
    const [result, setResult] = useState(0);
    const [temp, setTemp] = useState(10);
    var oops = 20;

    const handleClick = () => {
        console.log("按了一下")
        setResult(result + 1)
    };

    useEffect(() => {
        console.log("刷新了")
        setTemp(temp + result)
    }, [result]);

    console.log(result);
    console.log(temp);

    return (
        <>
            <p>{ result }</p>
            <button onClick={handleClick}>+1</button>
        </>
    )
}

在组件刚加载的时候,我们可以看到useEffect中的内容执行了一次:

接着我们点击按钮,每次点击都触发了result的改变,由于result在useEffect的[]参数中,所以useEffect中的内容会被执行:

 

现在我们把[]中的内容换成另一个不会因为点击而改变的变量oops:

import React, {useState, useEffect} from 'react';

export default function AddOn() {
    const [result, setResult] = useState(0);
    const [temp, setTemp] = useState(10);
    var oops = 20;

    const handleClick = () => {
        console.log("按了一下")
        setResult(result + 1)
    };

    useEffect(() => {
        console.log("刷新了")
        setTemp(temp + result)
    }, [oops]);

    console.log(result);
    console.log(temp);

    return (
        <>
            <p>{ result }</p>
            <button onClick={handleClick}>+1</button>
        </>
    )
}

我们会发现,除了刚加载时候的一次执行之外,由于oops没有变化过,所以useEffect中的内容就一直不执行了(temp一直是10):

那么根据这一点,如果我们直接用一个空的[],我们就可以令useEffect只执行一次,即在组件刚加载时运行一次,之后就再也不运行了。比如我们想从后端一次性地取一批数据过来(在之后的交互中不再取数据),就可以用这种方法。

 

当然,useEffect的[]中也可以加入多个值,只要任意一个更新了,那么useEffect中的内容就会被执行一次:

import React, {useState, useEffect} from 'react';

export default function AddOn() {
    const [result, setResult] = useState(0);
    const [something, setSomething] = useState(0);
    const [temp, setTemp] = useState(10);

    const handleClick = () => {
        console.log("按了一下")
        setResult(result + 1)
    };

    const handleSomethingClick = () => {
        console.log("按了个寂寞")
        setSomething(something + 1)
    };

    useEffect(() => {
        console.log("刷新了")
        setTemp(temp + result + something)
    }, [result, something]);

    console.log(result);
    console.log(something);
    console.log(temp);

    return (
        <>
            <p>{ result }</p>
            <button onClick={handleClick}>+result</button>
            <p>{ something }</p>
            <button onClick={handleSomethingClick}>+something</button>
        </>
    )
}

这里我们有两个按钮,点击后分别更新result和something,从console中我们可以看到,无论我们点击哪个按钮,最后都会更新temp的值,也就是说useEffect都会被触发。

 

useEffect的另一个主要作用,就是帮助渲染从后端拉取的数据。比如我有个很简单的后端:

var express = require('express');
var cors = require('cors');

var app = express();

var corsOptions = {
  credentials:true,
  origin:'http://localhost:3000',
  optionsSuccessStatus:200
};
app.use(cors(corsOptions));

app.use(express.urlencoded({extended: true})); // 必须要加
app.use(express.json()); // 必须要加

app.post('/', function (req, res) {
  let p = req.body.name
  res.send(`${p}很有精神`)
});

app.listen(5000, function() {
  console.log('App listening on port 5000...')
});

它有一个POST方法,即前端传入一个名字XXX,后端返回“XXX很有精神”。

我参考了一下实习生大佬的写法,他一般会在前端写一个Service文件,负责对接后端的API们:

import axios from 'axios';

const api = "http://localhost:5000";

class BackendService {
    postInfo(body) {
        return new Promise((resolve) => resolve(axios.post(`${api}`, body)));
    }
}

export default new BackendService();

然后再用一个自定义的hook负责加载和拉取(自定义的hook貌似都必须写成useXxx):

import BackendService from "./BackendService";
import { useState, useEffect } from 'react';

export default function useBackend(name) {
    const [info, setInfo] = useState(null);
    const [error, setError] = useState(null);

    console.log(info);

    useEffect(() => {
        BackendService.postInfo({"name":name})
        .then(response => {
            setInfo(response.data)
        })
        .catch(error => {
            setError("后端错误")
        })
    }, [name]);

    return [info, error];
}

从后端成功拉取response之后返回相应的内容,这里useEffect的[]中是name,也就是只要name变了,那就触发useEffect内部的内容。

我们再写一个组件来看看(新建一个BattleShip组件,再把它放入App.js中):

import React, { useState } from 'react';
import useBackend from "./useBackend";

export default function Battleship() {
    const [name, setName] = useState("森下下士");
    const info = useBackend(name);

    console.log(name);
    console.log(info);

    return (
        <div>
            <p>{ info }</p>
            <button onClick={ () => setName("天尊杨戬") }>天尊杨戬</button>
            <button onClick={ () => setName("天山新泰罗") }>天山新泰罗</button>
            <button onClick={ () => setName("挺甜一郎") }>挺甜一郎</button>
        </div>
    );
}

我们从后端拉取的内容会通过useBackend这个自定义hook拉入info中,我们可以看一下结果:

由于请求是异步的,所以一开始会先返回null(似乎可以理解成位置我先占了,事情我一会儿再做),后端的response来了之后再重渲染了页面。有时候可能会由于这个占位的null产生一些错误,一般加个条件判断就能解决。

 

3、useContext 跨组件

在我认识useContext之前,跨组件的获取/修改状态往往是个很蛋疼的问题,通过状态提升和props转来转去有时候把自己都绕晕了,而useContext则提供了一种相对简单的方法。

首先,我们来写一个StudentContext.js:

import React, { useState } from 'react';

const StudentContext = React.createContext();

function StudentContextProvider(props) {
    const [currentStudent, setCurrentStudent] = useState(null);

    return (
        <StudentContext.Provider value={{currentStudent:currentStudent, setCurrentStudent:setCurrentStudent}}>
            {props.children}
        </StudentContext.Provider>
    );
}

function StudentContextConsumer(props) {
    return (
        <StudentContext.Consumer>
            {props.children}
        </StudentContext.Consumer>
    );
}

export { StudentContextProvider, StudentContextConsumer, StudentContext };

我们可以把Context看做是个中转站,我们所需要的状态都被储存在Context里,而其他组件都连接至这个中转站,从而查询或者修改其中的状态。Context内部的本质其实也就是一个或者一堆useState。

在这里,我们导出的StudentContext是个Object,包含了2个内容,一个是变量currentStudent,另一个是设置currentStudent用的函数setCurrentStudent。

 

接下来,我们新建2个组件,一个负责查询Context中的状态:

import React, { useContext } from 'react';
import { StudentContext } from "./StudentContext";

export default function StudentCard() {
    var studentCxt = useContext(StudentContext);
    console.log(studentCxt);

    return (
        <div>{ studentCxt.currentStudent }</div>
    );
};

一个负责修改Context中的状态:

import React, { useContext } from 'react';
import { StudentContext } from "./StudentContext";

export default function CallStudent() {
    var studentCxt = useContext(StudentContext);

    return (
        <>
            <button onClick={ () => studentCxt.setCurrentStudent("天尊杨戬") }>福冈县</button>
            <button onClick={ () => studentCxt.setCurrentStudent("天山新泰罗") }>东京府</button>
            <button onClick={ () => studentCxt.setCurrentStudent("挺甜一郎") }>岩手县</button>
        </>
    )
};

最后,我们将这两个组件都放入一个父组件中,父组件被Context的Provider包起来,意味着内部的所有子组件都可以共享Context这个中转站。

import React, { useContext } from 'react';
import { StudentContextProvider } from "./StudentContext";
import StudentCard from "./StudentCard";
import CallStudent from "./CallStudent";

export default function Students(props) {
    return (
        <StudentContextProvider>
            <StudentCard/>
            <CallStudent/>
        </StudentContextProvider>
    );
};

看一下效果:

每次点击CallStudent组件中的按钮,都会更新Context中的状态,而这个状态会被传达到StudentCard这个组件中,并显示出来。这也就完成了跨组件的状态传递。

 

以上也就是近期的一些心得啦,真心觉得全栈还是很伟大的,有时候光前端实现某个功能就令人痛不欲生了,还要保证跟后端的同步连接和协调,真是太不容易了。路还很长,继续修炼~

代码见:

https://github.com/SilenceGTX/play_with_hooks

posted @ 2020-09-20 20:05  SilenceGTX  阅读(297)  评论(0编辑  收藏  举报