上周实习生大佬休假,导致疯狂赶工,在一如既往的复制-黏贴-修改中,竟也渐渐琢磨出一点前端的感觉来。这一期主要讲讲最近使用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
从零开始的野路子React/Node(5)近期Hooks使用体会
原文:https://www.cnblogs.com/silence-gtx/p/13701864.html