1. 程式人生 > 實用技巧 >從零開始的野路子React/Node(5)近期Hooks使用體會

從零開始的野路子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