基础语法
创建项目
借助脚手架,新建一个React项目(可以使用vite或者cra,这里使用cra)
npx create-react-app 项目名
create-react-app
是React脚手架的名称
启动项目
npm start 或者 yarn start
src
是源文件index.js
相当于Vue的main.js
文件。整个程序的入口App.js
相当于Vue的App.js
,根组件
{}表达式
-
里面可以写入方法,变量,三元,短路与(&&),短路或(||)等js表达式
-
表达式是可以产生一个值的js语句(也就是可以被函数返回)
import { useState } from 'react';
import './App.css';const App = () => {
const [num, setNum] = useState(100)
const [flag, setFlag] = useState(true)
const fn = () => {
return '方法执行了'
}return ( <div className="App"> <h3>{ }中可以使用定义好的变量</h3> { num } <h3>{ }中可以使用三元表达式</h3> { flag ? num : '无' } <h3>{ }中可以使用短路与</h3> { flag && num } <h3>{ }中可以使用短路或</h3> { !flag || num } <h3>{ }中可以使用方法</h3> { fn() } </div> );
}
export default App;
列表渲染
-
通过
map
进行遍历,里面需要绑定key
值,方便diff
算法进行对比,提高diff
性能 -
重复渲染那个模板,就
return
谁 -
key 在当前列表中要唯一的字符串或者数值(String/Number)
export default function App() {
const [list, setList] = useState([
{ id: 0, name: '张三' },
{ id: 1, name: '李四' },
{ id: 2, name: '王五' },
])
return (
)
}
条件渲染
-
根据是否满足条件生成HTML结构,比如Loading效果
-
可以使用 三元运算符 或 逻辑与(&&)运算符 或 逻辑或(||)运算符
export default function App() {
const [flag, setFlag] = useState(true)
return (
{flag ? '正确的' : null}
{flag && '前面只有为true的情况下才会执行'}
{!flag || '前面只有为false的情况下才会执行'}
)
}
if系列判断渲染
-
可以声明一个方法,接收值,内部进行判断,并返回对应的结果
export default function App() {
const getType = (type) => {
if (type === 0) {
return 111
} else if (type === 1) {
return 222
} else if (type === 2) {
return 333
}
}
return (
{getType(1)}
)
}
样式处理
-
利用
className
指定类名,适合样式比较多的情况 -
直接行内样式,适合样式比较少的
标题
-
单独声明一个样式类名对象
export default function App() {
let style = {
color: 'pink',
fontSize: 20,
}
return (
标题
)
}
动态类名
-
根据条件显示类名
export default function App() {
let style = {
color: 'pink',
fontSize: 20,
}
let [flag, setFlag] = useState(true)
return (
<h2 style={flag ? style : ''}>标题
)
}
动态类名插件
- 还有很多用法,可以查看npm搜索
注意事项
- JSX必须有一个根节点,如果没有根节点,可以使用<></>(幽灵节点)替代
- 所有标签必须形成闭合,成对闭合或者自闭合都可以
- JSX中的语法更加贴近JS语法,属性名采用驼峰命名法 class -> className for -> htmlFor
- JSX支持多行(换行),如果需要换行,需使用() 包裹,防止bug出现
小案例1
-
实现一个最基本的评论(不完整)
import './index.css'
import avatar from './images/avatar.png'
// 依赖的数据
const state = {
// hot: 热度排序 time: 时间排序
tabs: [
{
id: 1,
name: '热度',
type: 'hot',
},
{
id: 2,
name: '时间',
type: 'time',
},
],
active: 'hot',
list: [
{
id: 1,
author: '刘德华',
comment: '给我一杯忘情水',
time: new Date('2021-10-10 09:09:00'),
// 1: 点赞 0:无态度 -1:踩
attitude: 1,
},
{
id: 2,
author: '周杰伦',
comment: '哎哟,不错哦',
time: new Date('2021-10-11 09:09:00'),
// 1: 点赞 0:无态度 -1:踩
attitude: 0,
},
{
id: 3,
author: '五月天',
comment: '不打扰,是我的温柔',
time: new Date('2021-10-11 10:09:00'),
// 1: 点赞 0:无态度 -1:踩
attitude: -1,
},
],
}
// 时间格式化
const format = (time) => {
return${time.getFullYear()}-${ time.getMonth() + 1 < 10 ? '0' + time.getMonth() + 1 : time.getMonth() + 1 }-${time.getDate() < 10 ? '0' + time.getDate() : time.getDate()} ${ time.getHours() < 10 ? '0' + time.getHours() : time.getHours() }:${time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes()}:${ time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds() }
}
// tab切换
const activeClick = (active) => {
state.active = active
}function App() {
return (
{/* 评论数 /}
5 评论
{/ 排序 */}
{state.tabs.map((item) => {
return (
<li
className={state.active === item.type ? 'on' : ''}
onClick={() => activeClick('hot')}
key={item.id}
>
按{item.name}排序
)
})}
{/* 添加评论 */} <div className="comment-send"> <div className="user-face"> <img className="user-head" src={avatar} alt="" /> </div> <div className="textarea-container"> <textarea cols="80" rows="5" placeholder="发条友善的评论" className="ipt-txt" /> <button className="comment-submit">发表评论</button> </div> <div className="comment-emoji"> <i className="face"></i> <span className="text">表情</span> </div> </div> {/* 评论列表 */} <div className="comment-list"> {state.list.map((item) => { return ( <div className="list-item" key={item.id}> <div className="user-face"> <img className="user-head" src={avatar} alt="" /> </div> <div className="comment"> <div className="user">{item.author}</div> <p className="text">{item.comment}</p> <div className="info"> <span className="time">{format(item.time)}</span> <span className={item.attitude === 1 ? 'like liked' : 'like'} > <i className="icon" /> </span> <span className={item.attitude === -1 ? 'hate hated' : 'hate'} > <i className="icon" /> </span> <span className="reply btn-hover">删除</span> </div> </div> </div> ) })} </div> </div> </div> )
}
export default App
组件
- 分为函数式组件(rfc)和类组件(rnc)
- 安装
ES7+ React/Redux/React-Native snippets
这个插件后就可以使用上述指令快速创建组件 - 主要讲函数式组件。在react中,一个组件就是首字母大写的函数
绑定事件
on
开头,后面紧跟事件名(事件名首字母大写)
on事件名 = { 事件处理函数名 } // 无参
on事件名 = { () => 事件处理函数名(参数1,参数2...) } // 有参
on事件名 = { (e) => 事件处理函数名(e,参数2...) } // 有参,带e的
- 事件处理函数
let/const 事件处理函数名 = (参数) => { ... }
import React from 'react' export default function App() { const print = () => { console.log('无参的') } const hasParams = (e, num) => { console.log('有参的', e, num) } return ( <div> <button onClick={print}>print</button> <button onClick={(e) => hasParams(e, '123')}>hasParams</button> </div> ) }
小技巧
-
数值改变
-
数组添加
-
对象修改
import React from 'react'
import { useState } from 'react'export default function App() {
const [num, setNum] = useState(10)
const [list, setList] = useState([])
const [obj, setObj] = useState({
name: '张三',
})// 数值加几,可以直接在后面写加几 const numAdd = (n) => { setNum(num + n) } // 数组添加,可以直接在尾部添加 const listAdd = (item) => { setList([...list, item]) } // 修改对象中的某一项 const objEdit = (val) => { setObj({ ...obj, // 下面的会覆盖上面的同名的属性,达到修改的目的 name: val, }) } return ( <div> <button onClick={() => numAdd(1)}>数值加1--{num}</button> <div> <button onClick={() => listAdd('数组新的一项')}>数组添加一项</button> {list.map((item, i) => ( <p key={i}>{item}</p> ))} </div> <div> <button onClick={() => objEdit('李四')}> 修改对象的某一项(修改name) </button> <p>{obj.name}</p> </div> </div> )
}
-
数组删除(最好利用filter)
import React from 'react'
import { useState } from 'react'export default function App() {
const [list, setList] = useState([1,2,3])
// 删除数组中下标为2的内一项
const delItem = (index) => {
let newList = list.filter((item, i) => i !== 2)
setList(newList)
// 或者直接操作,也是可以的
// setList(list.filter((item, i) => i !== 2))
}return ( <div> <button onClick={() => delItem(2)}>删除数组中的某一项</button> </div> )
}
受控组件
-
被react状态控制的组件就叫受控组件。通过事件对象e,可以获取输入框中的值
import React from 'react'
import { useState } from 'react'export default function App() {
const [val, setVal] = useState('')
// 表单里面的值发生变化
const onChange = (e) => {
// 获得输入框中的值
console.log(e.target.value)
// 赋值给val
setVal(e.target.value)
}return ( <div> <input type="text" name="" id="" value={val} onChange={onChange} /> </div> )
}
非受控组件
-
不受react状态控制的组件叫非受控组件。通过获取dom元素,来获取输入框中的值
import React from 'react'
import { useRef } from 'react'export default function App() {
const ipt = useRef(null)
// 表单里面的值发生变化
const onChange = () => {
// 获得输入框中的值
console.log(ipt.current.value)
}return ( <div> <input type="text" name="" id="" ref={ipt} onChange={onChange} /> </div> )
}
小案例2
-
完整的评论功能
import './index.css'
import avatar from './images/avatar.png'
import { useState } from 'react'// 时间格式化
const format = (time) => {
return${time.getFullYear()}-${ time.getMonth() + 1 < 10 ? '0' + time.getMonth() + 1 : time.getMonth() + 1 }-${time.getDate() < 10 ? '0' + time.getDate() : time.getDate()} ${ time.getHours() < 10 ? '0' + time.getHours() : time.getHours() }:${time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes()}:${ time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds() }
}function App() {
// hot: 热度排序 time: 时间排序
const [tabs] = useState([
{
id: 1,
name: '热度',
type: 'hot',
},
{
id: 2,
name: '时间',
type: 'time',
},
])
const [list, setList] = useState([
{
id: 1,
author: '刘德华',
comment: '给我一杯忘情水',
time: new Date('2021-10-10 09:09:00'),
// 1: 点赞 0:无态度 -1:踩
attitude: 1,
},
{
id: 2,
author: '周杰伦',
comment: '哎哟,不错哦',
time: new Date('2021-10-11 09:09:00'),
// 1: 点赞 0:无态度 -1:踩
attitude: 0,
},
{
id: 3,
author: '五月天',
comment: '不打扰,是我的温柔',
time: new Date('2021-10-11 10:09:00'),
// 1: 点赞 0:无态度 -1:踩
attitude: -1,
},
])
// 切换的tab
const [active, setActive] = useState('hot')
// tab切换
const activeClick = (type) => {
setActive(type)
}
// 输入框的值
const [iptVal, setIptVal] = useState('')
// 得到输入框中的值
const getVal = (e) => {
setIptVal(e.target.value)
}
// 点击发送评论按钮
const sendCommit = () => {
if (!iptVal || iptVal.trim().length < 1) {
return alert('输入不能为空或都是空格')
}
setList([
...list,
{
id: +new Date(),
author: '孤勇者',
comment: iptVal,
time: new Date(),
// 1: 点赞 0:无态度 -1:踩
attitude: 0,
},
])
setIptVal('')
}
// 点击删除
const delItm = (id) => {
let newList = list.filter((item) => item.id !== id)
setList(newList)
}
// 点击点赞/点踩
const toggleMood = (item) => {
let { id, attitude } = item
let newList = list.map((item) => {
if (item.id === id) {
return {
...item,
attitude: attitude === 1 ? 0 : 1,
}
} else {
return item
}
})
console.log(newList)
setList(newList)
}return ( <div className="App"> <div className="comment-container"> {/* 评论数 */} <div className="comment-head"> <span>{list.length} 评论</span> </div> {/* 排序 */} <div className="tabs-order"> <ul className="sort-container"> {tabs.map((item) => { return ( <li className={active === item.type ? 'on' : ''} onClick={() => activeClick(item.type)} key={item.id} > 按{item.name}排序 </li> ) })} </ul> </div> {/* 添加评论 */} <div className="comment-send"> <div className="user-face"> <img className="user-head" src={avatar} alt="" /> </div> <div className="textarea-container"> <textarea cols="80" rows="5" placeholder="发条友善的评论" className="ipt-txt" onChange={getVal} value={iptVal} /> <button className="comment-submit" onClick={sendCommit}> 发表评论 </button> </div> <div className="comment-emoji"> <i className="face"></i> <span className="text">表情</span> </div> </div> {/* 评论列表 */} <div className="comment-list"> {list.map((item, index) => { return ( <div className="list-item" key={item.id}> <div className="user-face"> <img className="user-head" src={avatar} alt="" /> </div> <div className="comment"> <div className="user">{item.author}</div> <p className="text">{item.comment}</p> <div className="info"> <span className="time">{format(item.time)}</span> <span className={item.attitude === 1 ? 'like liked' : 'like'} onClick={() => toggleMood(item)} > <i className="icon" /> </span> <span className={item.attitude === -1 ? 'hate hated' : 'hate'} > <i className="icon" /> </span> <span className="reply btn-hover" onClick={() => delItm(item.id)} > 删除 </span> </div> </div> </div> ) })} </div> </div> </div> )
}
export default App
组件通信
- 父子通信,子父通信,非父子通信
父->子通信
- 父组件在子组件标签上绑定要传入的数据(会默认添加到props中),子组件通过
props
进行使用
父组件
import React from 'react' import { useState } from 'react' import Son from './pages/Son.jsx' export default function App() { const [num, setNum] = useState(100) return ( <div> <h2>App</h2> <p>下面是子组件</p> <Son num={num}></Son> </div> ) }
子组件
import React from 'react' export default function Son(props) { return ( <div> <h2>Son</h2> <p>从父组件传来的值是:{props.num}</p> </div> ) }
props详解
- props是只读对象(readonly)
-
- 是自顶向下单向数据流,根据单项数据流的要求,子组件只能读取props中的数据,不能进行修改
- 不同于Vue,react的props比较彻底,就完全不能修改。Vue如果传入对象类型数据,其实是可以修改的
- props可以传递任意数据
-
- 数字、字符串、布尔值、数组、对象、函数(多用子向父传值) 、JSX(类似于Vue的插槽)
父组件
import React,{ useState } from 'react' import Son from './pages/Son.jsx' export default function App() { // 数字 const [num, setNum] = useState(100) // 字符 const [str, setStr] = useState('str') // 布尔 const [bool, setBool] = useState(false) // 数组 const [list, setList] = useState([1, 2, 3]) // 对象 const [obj, setObj] = useState({ name: '张三', age: 24 }) // 函数 const print = () => { return 'print' } // jsx const jsx = <span>123</span> return ( <div> <h2>App</h2> <p>下面是子组件</p> <Son num={num} str={str} bool={bool} list={list} obj={obj} print={print} jsx={jsx} > 直接写在标签内的jsx结构,会自动传入到props中的children属性里(或者子组件标签上写children属性,一样的效果) </Son> </div> ) }
子组件
import React from 'react' // 也可以直接在参数这里解构 export default function Son(props) { let { num, str, bool, list, obj, print, jsx } = props return ( <div> <h2>Son</h2> <p>从父组件传来的值是:{num}</p> <p>从父组件传来的值是:{str}</p> <p>从父组件传来的值是:{bool ? '是true' : '是false'}</p> <p>从父组件传来的数组,渲染结果如下</p> <ul> {list.map((item) => ( <li key={item}>{item}</li> ))} </ul> <p>从父组件传来的对象,渲染结果如下</p> {obj.name} --- {obj.age} <p>从父组件传来的函数,渲染结果如下</p> {print()} <p>父组件传来的jsx结构,如下</p> {jsx} <p>父组件传过来的jsx结构,如下</p> {props.children} </div> ) }
子->父通信
- 也是通过props,通过传递函数进行通信
- 子组件调用父组件传递过来的函数,并且把想要传递的数据当成函数的实参
- 本质就是子组件调用了父组件传递过来的有参方法,只不过参数是子组件提供,从而达到子向父传值的作用
子组件
import React,{ useState } from 'react' export default function Son(props) { let [msg, setMsg] = useState('子组件向父组件传递的数据') const sendMsg = () => { props.getMsg(msg) } return ( <div> <h2>Son</h2> <button onClick={sendMsg}>点击向父组件传值</button> </div> ) }
父组件
import React,{ useState } from 'react' import Son from './pages/Son' export default function App() { let [sonData, setSonData] = useState({}) const getMsg = (val) => { console.log(val) setSonData({ ...sonData, msg: val }) } return ( <div> <h2>App --- {sonData.msg}</h2> <Son getMsg={getMsg}></Son> </div> ) }
兄弟组件通信
- 通过状态提升,利用共同的父组件实现兄弟通信
- 兄弟组件A -> 父组件 -> 兄弟组件B
子组件A
import React, { useState } from 'react' export default function SonA(props) { const [msgA, setMsgA] = useState('兄弟组件A传递的数据') const sendB = () => { props.getMsgA(msgA) } return ( <div> <h3>Son1</h3> <button onClick={sendB}>点击发送给兄弟组件B</button> </div> ) }
父组件
import React, { useState } from 'react' import SonA from './pages/SonA' import SonB from './pages/SonB' export default function App() { const [msgA, setMsgA] = useState('') // 接收A组件传来的值 const getMsgA = (val) => { setMsgA(val) } return ( <div> <h2>App</h2> <SonA getMsgA={getMsgA}></SonA> <SonB msgA={msgA}></SonB> </div> ) }
子组件B
import React from 'react' export default function SonB(props) { return ( <div> <h3>Son2</h3> <p>接收兄弟组件B传来的值为:{props.msgA}</p> </div> ) }
跨组件通信Context
- 直接在
index.js
文件中提供数据,则全局都可以使用
-
- 适用于只是用1次的静态的数据
- 如果提供的数据需要维护状态,则写到
app.js
中 - 只要是嵌套关系,都可以通过这个实现通信
使用步骤
-
首先创建一个独立的文件,
Context.js
import { createContext } from 'react'
const Context = createContext()
export default Context
-
那个组件需要就直接导入,
Provider
标签包裹根组件,value
提供数据(提供的数据比较多,就可以使用对象形式)import React, { useState } from 'react'
import Son from './pages/Son'
// 1. 引入Context
import Context from './utils/context'export default function App() {
const [msg, setMsg] = useState('根组件传递的数据')return (
<>
{/* 2. 使用Provider包裹上层组件提供数据 /}
<Context.Provider value={msg}>
{/ 根组件 */}
App
<Son></Son>
</Context.Provider>
</>
)
} -
数据消费组件,用
useContext
这个hook,或者通过Consumer
标签接收显示数据
使用 useContext****这个hook
import React, { useContext } from 'react' import Sun from './Sun' import Context from '../utils/context' export default function Son() { let val = useContext(Context) return ( <div> <h3>Son</h3> <p>从根组件得到的数据 --- {val}</p> <Sun></Sun> </div> ) }
通过 Consumer****标签
import React from 'react' import Context from '../utils/context' export default function Sun() { return ( <div> <h5>Sun</h5> <div> 从根组件得到的数据: <Context.Consumer>{(value) => <span>{value}</span>}</Context.Consumer> </div> </div> ) }
组件进阶
children属性
- 表示该组件的子节点,只要组件内部有子节点,props中就有该属性
children
属性,类似于插槽。直接写在标签中的内容会填充到children
属性上面children
可以是普通文本 ,普通标签元素 ,函数 / 对象 ,JSX- 如果并列的传入多个,
props
的children
属性会变成一个数组(就可以直接进行遍历)
父组件
import React from 'react' import Son from './pages/Son' export default function App() { return ( <div> <h2>App</h2> <Son> 普通文本:666 <div>普通标签元素</div> {/* 函数 */} {function fn() { console.log('函数打印') }} {/* JSX结构 */} { <div> <p>{'这是一个普通的jsx结构'}</p> </div> } </Son> </div> ) }
子组件
import React from 'react' export default function Son(props) { console.log(props) return ( <div> <h3>Son</h3> <p>{props.children[0]}</p> {props.children[1]} {props.children[2]()} {props.children[3]} <hr /> {props.children.map((item) => { return item })} </div> ) }
props校验
- 下载 prop-types 插件 ,并导入prop-types包 yarn add prop-types
- 使用 组件名.propsTypes = { } 来给组件的props中的数据添加校验规则
- 校验规则通过PropTypes对象来指定
检验基本语法
组件名.prototype = {
属性名 : PropTypes.XXX,
}
PropTypes
是引入的prop-types
插件的实例
设置默认值
组件名.defaultProps= {
属性名 : 默认值,
}
- 或者在参数上直接给默认值
父组件
import React, { useState } from 'react' import Son from './pages/Son' export default function App() { const [list] = useState([ { id: 0, name: '张三', }, { id: 1, name: '李四', }, ]) const [obj] = useState({ name: '王五', age: 24, }) return ( <div> <h2>App</h2> <Son list={list} score={100} obj={obj}></Son> </div> ) }
子组件
import React from 'react' import PropTypes from 'prop-types' export default function Son(props) { let { list, score, obj } = props return ( <div> <h3>Son</h3> <ul> {list.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> <p>成绩是:{score}</p> <p>姓名:{obj.name}</p> <p>年龄:{obj.age}</p> </div> ) } // 对传过来的值进行校验 Son.propTypes = { list: PropTypes.array.isRequired, // 也可以自定义校验规则 peops是所有接收过来的数据,propsName是字段名,componentName组件名 score: function (props, propsName, componentName) { if (props[propsName] < 60) { return new Error('成绩不合格') } }, obj: PropTypes.shape({ name: PropTypes.string, age: PropTypes.number, }), } // 设置默认值 Son.defaultProps = { list: [], score: 100, }
常见规则
-
常见类型: array bool func number object string
-
React元素类型(JSX): element
-
是否必填: isRequired
-
特定结构的对象: shape({ }) 也就是指定对象里面字段的规则,可以指定一个,也可以指定多个
// 特定结构的对象
obj: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number,
}), -
也可以自定义校验规则(见上面的例子)
Hook
useState
useState(初始值)
返回值是一个数组(里面有两项)[数据,修改数据的方法]
是对useState
进行结构。把里面的两项分别结构出来- 对于对象类型的状态变量,应该始终传给set方法一个全新的对象(深度拷贝)来进行修改
格式:
let/const [ 数据 ,修改数据的方法 ] = useState(默认值)
import React from 'react' import { useState } from 'react' export default function App() { const [count, setCount] = useState(0) const [obj,setObj] = useState({ name: 'zs', age: 25 }) console.log(useState(0)) // (2) [0, ƒ] const add = (num) => { let newCount = count + num setCount(newCount) } const changeObj = () => { setObj({ ...obj, age: 26 }) } return ( <div> <h2>App --- {count} -- {obj}</h2> <button onClick={() => add(1)}>+1</button> <button onClick={changeObj}>修改对象属性</button> </div> ) }
函数做为参数
useState
中也可以传入一个函数做为参数(初始值可能需要经过一些计算而得)- 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过计算才能获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
格式:
const [name, setName] = useState(()=>{
// 编写计算逻辑 return '计算之后的初始值'
})
父组件
import React from 'react' import { useState } from 'react' import Son from './pages/Son' export default function App() { const [count, setCount] = useState(0) const countEdit = (num) => { setCount(num) } return ( <div> <h2>App</h2> <button onClick={() => countEdit(10)}>10</button> <button onClick={() => countEdit(20)}>20</button> <Son count={count}></Son> </div> ) }
子组件
import React from 'react' import { useEffect } from 'react' import { useState } from 'react' export default function Son(props) { const [c, setc] = useState(() => props.count) useEffect(() => { setc(props.count) }, [props]) return ( <div> <h3>Son -- {c}</h3> </div> ) }
useEffect
useEffect
函数的作用就是为react函数组件提供副作用处理的useEffect
都是在组件dom渲染更新完毕之后才执行的
副作用
副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用。对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM,ajax请求)
常见的副作用
- 数据请求 ajax发送
- 手动修改dom
- localstorage操作
执行时机
1.不添加依赖项
- 组件首次渲染执行一次 ,以及不管是哪个状态更改引起组件更新时都会重新执行
-
- 组件初始渲染
- 组件更新 (状态数据变化引起的重新渲染,不管结构中使用没使用这个数据状态)
useEffect(()=>{
console.log('副作用执行了')
}) -
添加空数组
-
组件只在首次渲染时执行一次
useEffect(()=>{
console.log('副作用执行了')
},[])
- 添加特定依赖项
- 副作用函数在首次渲染时执行 ,在依赖项发生变化时重新执行
-
- 组件初始渲染
- 依赖项发生变化时
function App() {
const [count, setCount] = useState(0)
const [name, setName] = useState('zs')useEffect(() => { console.log('副作用执行了') }, [count]) return ( <> <button onClick={() => { setCount(count + 1) }}>{count}</button> <button onClick={() => { setName('cp') }}>{name}</button> </> )
}
注意事项
- useEffect 回调函数中用到的状态数据(比如,count)就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会有bug出现
清除副作用
- 在组件被销毁时,如果有些副作用操作需要被清除(比如定时器)
语法:
useEffect(() => {
// 副作用操作...
return () => {
// 写清除副作用的代码
}
})
eg: 清除定时器案例
父组件
import React from 'react' import { useState } from 'react' import Son from './pages/Son' export default function App() { const [flag, setFlag] = useState(true) return ( <div> <h2>App</h2> <button onClick={() => setFlag(!flag)}>显示/隐藏组件</button> {flag && <Son></Son>} </div> ) }
子组件
import React, { useEffect } from 'react' export default function Son() { // 组件进来的时候触发一个定时器 useEffect(() => { let timer = setInterval(() => { console.log('定时器执行了') }, 1000) // 组件销毁时清除定时器 return () => { // 在return里面的函数里写清除操作 clearInterval(timer) } }, []) return ( <div> <h3>Son</h3> </div> ) }
useEffect
发送网络请求-
依赖项要是一个空数组,因为依赖项为空数组时只会在页面初始化时触发一次
import React from 'react'
import { useEffect } from 'react'export default function App() {
const getData = () => {
fetch('https://cnodejs.org/api/v1/topics')
.then((response) => response.json())
.then((data) => console.log(data.data))
}
useEffect(() => {
getData()
}, [])return ( <div> <h2>App</h2> </div> )
}
案例1
-
求卷去头部距离的hook
import { useState } from 'react'
export default function useWindowScroll() {
const [y, sety] = useState('')
window.addEventListener('scroll', function () {
sety(this.document.documentElement.scrollTop)
})
return [y]
}使用
import React from 'react'
import useWindowScroll from './hook/useWindowScroll'export default function App() {
const [y] = useWindowScroll()
return (
<div style={{ height: 1600 }}>
App -- {y}
)
}案例2
-
数据改变,会同步到本地
import { useEffect, useState } from 'react'
export default function useLocalStorage(key, defaultVal) {
const [val, setVal] = useState(defaultVal)
// 只要val发生变化,就同步到本地
useEffect(() => {
localStorage.setItem(key, val)
}, [val, key])
return [val, setVal]
}使用
import React from 'react'
import useWindowScroll from './hook/useWindowScroll'
import useLocalStorage from './hook/useLocalStorage'export default function App() {
const [y] = useWindowScroll()
const [val, setVal] = useLocalStorage('val', 0)
const add = () => {
setVal(val + 1)
}return ( <div style={{ height: 1600 }}> <h2> App -- {y} -- {val} </h2> <button onClick={add}>+1</button> </div> )
}
useRef
-
可以获取元素的真实Dom
import React, { useEffect, useRef } from 'react'
export default function App() {
const ipt = useRef(null)
useEffect(() => {
console.log(ipt.current.value)
}, [])return ( <div> <h2>App</h2> <input type="text" ref={ipt} /> </div> )
}
useContext
- 传输的数据是响应式的,跨组件传输数据用
- 如果传递的数据,只需要在整个应用初始化的时候传递一次就可以,则可以在
index.js
文件中提供数据 - 如果传递的数据需要状态维护,则可以在
app.js
中提供数据
使用步骤
- 创建一个
context
的文件 - 使用
createContext
创建Context
对象,并导出 - 在顶层组件引入,通过
Provider
提供数据 - 在底层组件引入,通过
useContext
函数获取数据
举例
context.js
import { createContext } from 'react' const Context = createContext() export default Context
上层组件
import React, { useState } from 'react' import Son from './pages/Son' // 1. 引入Context import Context from './utils/context.js' export default function App() { const [msg] = useState('根组件传递的数据') return ( <> {/* 2. 使用Provider包裹上层组件提供数据 */} <Context.Provider value={msg}> {/* 根组件 */} <div> <h2>App</h2> <Son></Son> </div> </Context.Provider> </> ) }
下层组件
import React, { useContext } from 'react' import Context from '../utils/context.js' export default function Son() { let val = useContext(Context) return ( <div> <h3>Son</h3> <p>从根组件得到的数据 --- {val}</p> </div> ) }
hook使用总结
补充
document.title
可以获取网页最左上的标题
ReactTookit
首先进行安装
toolkit
和react-redux
npm install @reduxjs/toolkit react-redux
Redux Toolkit 示例
在
src
目录下新建stroe
文件夹,store
文件夹下新建index.js
和modules
(模块)文件夹src >> stroe >> index.js / mudules
1. 创建 Redux Store(redux仓库)
-
在我们新建的
store
文件夹下的index.js
里粘贴即可import { configureStore } from '@reduxjs/toolkit'
// 使用configureStore创建一个redux仓库
// 并自动配置了 Redux DevTools 扩展 ,这样你就可以在开发时调试 store
export default configureStore({
// 此时我们还没有写入 reducer 后面这里再写入,有一个就写一个,也可以写入多个,在reducer大括号里写入
reducer: {},
})
2. 为 React 提供 Redux Store
- 新建完仓库之后,并没有与
React
产生关联,所以我们需要产生关联,这样React
才能进行使用Toolkit
在
index.js
中进行关联import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' // 引入数据仓库,只有文件夹,默认会引入文件夹下的index.js import store from './store' // 结构出提供者组件,它有一个stroe属性,属性值就是我们要传递的值 import { Provider } from 'react-redux' ReactDOM.render( {/* 包裹App组件,这样全局都能进行使用 */} <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
3. 创建 Redux State Slice(切片)
-
切片可以理解为模块
-
在
modules
文件夹下新建counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
// 创建react数据切片 利用createSlice()
export const counterSlice = createSlice({
// 类似于vuex的命名空间,必须是唯一值
// 与pinia的defineStore()的第一个参数一个意思,都是唯一值,做区分
name: 'counter',
// 定义变量
initialState: {
value: 0,
},
// 定义方法
reducers: {
// 方法接收2个参数,第一个参数是变量,第二个参数是载荷(也就是使用方法传入的参数)
// +1
increment: (state) => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
// 并不是真正的改变状态值,因为它使用了 Immer 库
// 可以检测到"草稿状态" 的变化并且基于这些变化生产全新的
// 不可变的状态
// 大致意思就是可以直接修改原先的值,它会产生新的不可变状态,放心大胆修改
state.value += 1
},
// -1
decrement: (state) => {
state.value -= 1
},
// 这种是使用action的时候传参的
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
// 每个 case reducer 函数会生成对应的 Action creators 需要进行导出,这样组件使用方法或者变量的时候,直接引入就可了
export const { increment, decrement, incrementByAmount } = counterSlice.actionsexport default counterSlice.reducer
4. 将 Slice Reducers 添加到 Store 中
下一步,我们需要从计数切片中引入 reducer 函数,并将它添加到我们的 store 中。通过在 reducer 参数中定义一个字段,我们告诉 store 使用这个 slice reducer 函数来处理对该状态的所有更新
import { configureStore } from '@reduxjs/toolkit' // 引入 reducer 函数 import counterSlice from './modules/counterSlice' // 使用configureStore创建一个redux仓库 // 并自动配置了 Redux DevTools 扩展 ,这样你就可以在开发时调试 store export default configureStore({ reducer: { // 告诉 store 使用这个 slice reducer 函数来处理对该状态的所有更新 counter: counterSlice, }, })
5. 在 React 组件中使用 Redux 状态和操作
现在我们可以使用 React-Redux 钩子让 React 组件与 Redux store 交互。我们可以使用
useSelector
从 store 中读取数据,使用useDispatch
dispatch actions。import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { decrement, increment } from '../store/modules/counterSlice' export default function Person() { const count = useSelector((state) => state.counter.value) const dispatch = useDispatch() return ( <div> <h2>Person -- {count}</h2> <button // aria-label="Increment value" onClick={() => dispatch(increment())} > 增加 </button> <button // aria-label="Decrement value" onClick={() => dispatch(decrement())} > 减少 </button> </div> ) }
如何处理异步任务
counterSlice.js
import { createSlice } from '@reduxjs/toolkit' // 创建react数据切片 利用createSlice() export const counterSlice = createSlice({ // 类似于vuex的命名空间,必须是唯一值 // 与pinia的defineStore()的第一个参数一个意思,都是唯一值,做区分 name: 'counter', // 变量 initialState: { value: 0, }, // 方法 reducers: { // 方法接收2个参数,第一个参数是变量,第二个参数是载荷(也就是使用方法传入的参数) increment: (state) => { // Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它 // 并不是真正的改变状态值,因为它使用了 Immer 库 // 可以检测到"草稿状态" 的变化并且基于这些变化生产全新的 // 不可变的状态 state.value += 1 }, decrement: (state) => { state.value -= 1 }, incrementByAmount: (state, action) => { state.value += action.payload }, // 获取数据的异步请求方法 getD: (state, action) => { fetch('https://cnodejs.org/api/v1/topics') .then((response) => response.json()) .then((res) => console.log(res)) }, }, }) // 每个 case reducer 函数会生成对应的 Action creators export const { increment, decrement, incrementByAmount, getD } = counterSlice.actions // 定义异步任务,参数写道外层函数上,利用闭包 export const getData = (payload) => { // 内层函数,第一个参数是提交任务的dispatch,第二个参数是获取state的方法 return (dispatch, getState) => { // 获取state中的数据 console.log(getState().counter) // 调用方法 dispatch(getD()) } } export default counterSlice.reducer
使用
import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { decrement, increment, getData } from '../store/modules/counterSlice' export default function Person() { const count = useSelector((state) => state.counter.value) const dispatch = useDispatch() return ( <div> <h2>Person -- {count}</h2> <button onClick={() => dispatch(increment())}>增加</button> <button onClick={() => dispatch(decrement())}>减少</button> <button onClick={() => dispatch(getData())}>获取数据</button> </div> ) }
结果
路由
实现一个简单路由
- 先进行安装
npm i react-router-dom
- 创建page文件夹,然后新建需要的页面
-
创建router文件夹,新建index.js,然后在index.js中写入路由规则
import { createBrowserRouter } from 'react-router-dom'
import Login from '../page/Login'
import Article from '../page/Article'
import Home from '../page/Home'// 使用的浏览器路由模式
const routes = createBrowserRouter([
{
path: '/',
element: <Home />,
},
{
path: '/login',
element: <Login />,
},
{
path: '/article',
element: ,
},
])export default routes
-
在index.js中,引入路由规则和路由注入方法,并进行配置
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
// 引入路由规则数组
import routes from './router'
// 引入路由注入方法
import { RouterProvider } from 'react-router-dom'// 引入数据仓库,只有文件夹,默认会引入文件夹下的index.js
import store from './store'
// 结构出提供者组件,它有一个stroe属性,属性值就是我们要传递的值
import { Provider } from 'react-redux'const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={routes}></RouterProvider>
</Provider>
</React.StrictMode>
) -
修改app.js(不修改也没事,直接删除也行,反正一进来就匹配的Home页面)
export default function App() {
return <></>
}
路由跳转
- 分为声明式导航和编程式导航
声明式导航
-
通过Link标签的to属性指定跳转的路由path
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from '../../store/modules/counterSlice'
import { Link } from 'react-router-dom'export default function Home() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()return ( <div> <h2>Person -- {count}</h2> <button onClick={() => dispatch(increment())}>增加</button> <button onClick={() => dispatch(decrement())}>减少</button> <Link to="/login">跳转到登录页</Link> <Link to="/article">跳转到文章</Link> </div> )
}
编程式导航
-
通过useNavigate这个方法来进行跳转
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from '../../store/modules/counterSlice'
import { Link, useNavigate } from 'react-router-dom'export default function Home() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()const navigate = useNavigate() const toLogin = () => { navigate('/login') } return ( <div> <h2>Person -- {count}</h2> <button onClick={() => dispatch(increment())}>增加</button> <button onClick={() => dispatch(decrement())}>减少</button> <Link to="/login">跳转到登录页</Link> <button onClick={() => navigate('/article')}>跳转到文章页</button> <button onClick={toLogin}>跳转到登录页</button> {/* 下面这样写是不行的 */} {/* <button onClick={navigate('/login')}>跳转到登录页</button> */} </div> )
}
路由传参
- 可以在路由后面使用?拼接参数。也可以使用/路径参数形式
?形式拼接传参
传递参数
import { useSelector, useDispatch } from 'react-redux' import { decrement, increment } from '../../store/modules/counterSlice' import { Link, useNavigate } from 'react-router-dom' export default function Home() { const count = useSelector((state) => state.counter.value) const dispatch = useDispatch() const navigate = useNavigate() return ( <div> <h2>Person -- {count}</h2> <button onClick={() => dispatch(increment())}>增加</button> <button onClick={() => dispatch(decrement())}>减少</button> <Link to="/login?name=zs&age=25">跳转到登录页</Link> <button onClick={() => navigate('/article?name=ls&age=26')}> 跳转到文章页 </button> </div> ) }
结果:会显示在地址栏上
接收参数
import React from 'react' import { Outlet, useSearchParams } from 'react-router-dom' export default function Article() { const [params] = useSearchParams() const name = params.get('name') return ( <div> 文章页面{name} <Outlet></Outlet> </div> ) }
/路径参数传参
传递参数
import { useSelector, useDispatch } from 'react-redux' import { decrement, increment } from '../../store/modules/counterSlice' import { Link, useNavigate } from 'react-router-dom' export default function Home() { const count = useSelector((state) => state.counter.value) const dispatch = useDispatch() const navigate = useNavigate() return ( <div> <h2>Person -- {count}</h2> <button onClick={() => dispatch(increment())}>增加</button> <button onClick={() => dispatch(decrement())}>减少</button> <Link to="/login/100">跳转到登录页</Link> <button onClick={() => navigate('/article/100/1')}>跳转到文章页</button> </div> ) }
也要修改下路由配置,改为动态路由形式
import { createBrowserRouter } from 'react-router-dom' import Login from '../page/Login' import Article from '../page/Article' import Home from '../page/Home' // 使用的浏览器路由模式 const routes = createBrowserRouter([ { path: '/', element: <Home />, }, { path: '/login/:id', element: <Login />, }, { path: '/article/:num/:id', element: <Article />, }, ]) export default routes
结果:会显示在地址栏上
接收参数
Login:
import React from 'react' import { useParams } from 'react-router-dom' export default function Login() { const params = useParams() const id = params.id return <div>登陆页面{id}</div> }
Article:
import React from 'react' import { Outlet, useParams } from 'react-router-dom' export default function Article() { const params = useParams() const name = params.name const id = params.id return ( <div> 文章页面{id}{name} <Outlet></Outlet> </div> ) }
嵌套路由
配置嵌套路由
import { createBrowserRouter } from 'react-router-dom' import Login from '../page/Login' import Article from '../page/Article' import Home from '../page/Home' // 使用的浏览器路由模式 const routes = createBrowserRouter([ { path: '/', element: <Home />, }, { path: '/login/:id', element: <Login />, // 嵌套路由 children: [ { // 不需要带 / 了 path: 'test', element: <Article />, }, ], }, { path: '/article/:id/:name', element: <Article />, }, ]) export default routes
测试
配置路由出口:在对应的页面配置
子路由内容就出来了
设置初始默认页面
去除path,然后添加index为true
import { createBrowserRouter } from 'react-router-dom' import Login from '../page/Login' import Article from '../page/Article' import Home from '../page/Home' // 使用的浏览器路由模式 const routes = createBrowserRouter([ { path: '/', element: <Home />, }, { path: '/login/:id', element: <Login />, // 嵌套路由 children: [ { // 不需要带 / 了 // path: 'test', index: true, element: <Article />, }, ], }, { path: '/article/:id/:name', element: <Article />, }, ]) export default routes
测试
404页面
- 一定要放到最后
两种路由模式
Hook补充
useReducer
- 作用:和useState的作用类似,用来管理相对复杂的状态数据
案例
import { useReducer } from 'react' import { Button } from 'antd' const Home = () => { const fn = (state, action) => { // 根据传入的不同类型,来进行对应的数据操作 switch (action.type) { case 'INC': return state + 1 case 'DEC': return state - 1 default: return state } } // num是值,dispatchNum是修改状态的方法(参考useState) const [num, dispatchNum] = useReducer(fn, 0) const changeNum = (params) => { dispatchNum(params) } return ( <div> <h3>{num}</h3> <Button onClick={() => changeNum({ type: 'INC' })}>num++</Button> <Button onClick={() => changeNum({ type: 'DEC' })}>num--</Button> </div> ) } export default Home
useMemo
- 作用:在组件每次重新渲染的时候缓存计算的结果(缓存的是值,useCallback缓存的是函数)
首先先看一个场景
- count1与函数相关,所以count1变化的时候会触发函数
- 但是count2并没有与函数有什么关联,但是count2变化了,同样也会触发函数的执行,以达到视图的更新。但是这样显然是不合理的。此时就可以使用uesMemok来进行优化
基本语法
案例
import React, { useState } from 'react' import { Button } from 'antd' export default function Home() { const [count1, setCount1] = useState(0) const [count2, setCount2] = useState(0) console.log('组件重新渲染了') const fn = () => { console.log('函数调用了' + count1) } return ( <div> <h2>Home{fn()}</h2> <Button onClick={() => setCount1(count1 + 1)}>改变count1,count1++</Button> <Button onClick={() => setCount2(count2 + 1)}>改变count2,count2++</Button> </div> ) }
- fn函数只与count1有关,所以count1变化的时候触发fn函数是正确的。但是经过测试,会发现count2变化的时候也会触发fn函数,这是不对啊
就可以使用useMome对上面的进行优化
import React, { useMemo, useState } from 'react' import { Button } from 'antd' export default function Home() { const [count1, setCount1] = useState(0) const [count2, setCount2] = useState(0) console.log('组件重新渲染了') const fn = useMemo(() => { console.log('函数调用了' + count1) }, [count1]) return ( <div> {/* 由于缓存的是值,所以调用也需要改下 */} <h2>Home{fn}</h2> <Button onClick={() => setCount1(count1 + 1)}>改变count1,count1++</Button> <Button onClick={() => setCount2(count2 + 1)}>改变count2,count2++</Button> </div> ) }
React.memo
- 默认只要父组件有状态更新,子组件就会直接重新渲染,存在性能浪费
测试结果
useMomo基本语法
案例:
测试结果:
React.memo对比机制
- 传递给子组件基本类型数据,如果没有改变,确实不会重新渲染(可以看上面的案例)
- 但是传递给子组件复杂数据类型,如果没用useSate修饰的,则不管值变没变,都会重新渲染
测试结果:传递给子组件的count和list并没有改变,但是也触发了子组件渲染
为了解决这个问题,就可以采用useMemo配合React.memo
最终方案
测试结果:
useCallback
- 与useMemo不同,useCallback缓存的是函数引用,不是值
React.forwardRef
- 使用ref暴露DOM节点给父组件。可以通过React.forwardRef获取子组件DOM
实现
useImperativeHandle
- 通过ref暴露子组件中的方法,区别与React.forwardRef(这个是暴露的子组件DOM)
案例
React与Ts
创建项目(使用的vite)
npm create vite@latest 项目名 -- --template react-ts
useState类型
- 通常React会根据传入useState的默认值来自动推导类型,不需要显式标注类型
泛型参数
-
useState本身是一个泛型函数
import { useState } from 'react'
interface IObj {
name: string
age: number
}function App() {
const [obj] = useState<IObj>({
name: 'zs',
age: 25,
})return ( <> <h2>App</h2> {obj.name} -- {obj.age} </> )
}
export default App
补充,其实setState里也可以传入一个函数,只要函数返回一个值就行
初始值为null
-
有时候,state的初始值设置为了null。则类型就可以使用联合类型
import { useState } from 'react'
interface IObj {
name: string
age: number
}function App() {
const [obj] = useState<IObj | null>(null)return ( <> <h2>App</h2> {obj?.name} -- {obj?.age} </> )
}
export default App
props类型
案例
import { useState } from 'react' interface IProps { count: number } function App() { const [count] = useState(100) return ( <> <h2>App</h2> <Son count={count}></Son> </> ) } function Son(props: IProps) { return ( <> <h3>子组件 {props.count}</h3> </> ) } export default App
为children添加类型
为事件添加类型
案例
import { useState } from 'react' interface IProps { count: number children: React.ReactNode getMsg?: (str: string) => void } function App() { const [count] = useState(100) const getMsg = (msg: string) => { console.log('接收到了子组件的值:', msg) } return ( <> <h2>App</h2> <Son count={count} getMsg={getMsg}> 传给子组件的内容 </Son> </> ) } function Son(props: IProps) { const { getMsg } = props const sendMsg = () => { getMsg?.('子向父传的值') } return ( <> <h3>子组件 {props.count}</h3> <button onClick={sendMsg}>传给父组件的值</button> </> ) } export default App
useRef类型
获取Dom使用
案例
import { useEffect, useRef } from 'react' function App() { const inputRef = useRef<HTMLInputElement>(null) useEffect(() => { inputRef.current?.focus() }, []) return ( <> <h2>App</h2> <input ref={inputRef}></input> </> ) } export default App
当成稳定的存储器使用
axios类型
组合成了一种类型
案例
封装axios
import axios from 'axios' const requestInstance = axios.create({ baseURL: 'http://geek.itheima.net/v1_0', timeout: 5000, }) requestInstance.interceptors.request.use( (config) => { return config }, (error) => { return Promise.reject(error) } ) requestInstance.interceptors.response.use( (response) => { return response }, (error) => { return Promise.reject(error) } ) export default requestInstance
apis
import { http } from '@/utils' // 定义通用泛型接口 interface IResType<T> { data: T message: string } // 定义data类型 interface IObj { id: number name: string } interface IDataType { channels: Array<IObj> } // 请求频道列表 export const getChannels = () => http.request<IResType<IDataType>>({ url: '/channels', })