從零開始的野路子React/Node(5)近期Hooks使用體會
上週實習生大佬休假,導致瘋狂趕工,在一如既往的複製-黏貼-修改中,竟也漸漸琢磨出一點前端的感覺來。這一期主要講講最近使用Hooks的心得。
(以下梗皆出自B站最近挺火的《啊!海軍》)
1、useState 監聽自身的改變
useState可以視作專門監聽某一個變數的改變,當其發生變化時,重新渲染頁面。
useState監聽的這個變數不僅僅可以是簡單型別(比如整數,字串……)也可以是一個Object。這也就意味著其實useState可以同時監聽多個值,比如(新建一個UpAndDown元件,再把它放入App.js中):
import React, {useState} from 'react'; exportdefault 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這個元件中,並顯示出來。這也就完成了跨元件的狀態傳遞。
以上也就是近期的一些心得啦,真心覺得全棧還是很偉大的,有時候光前端實現某個功能就令人痛不欲生了,還要保證跟後端的同步連線和協調,真是太不容易了。路還很長,繼續修煉~
程式碼見: