一些介绍
本文写了这么些东西:
1.介绍React Hooks,除了useImperativeHandle、useLayoutEffect和useDebugValue
2.介绍Redux
3.介绍Router
4.结合上面的东西实现一个小demo
尽量不讲概念,简单粗暴实现。已经如果你之前已经了解组件化思想,或者有用过Vue,读起来会省力一些。
参考资料:
1.官方文档
2.极简入门react:B站地址
3.每一个解答我疑问的旁友
Hooks
useState与useReducer
改变变量并触发视图更新,但useReducer
更适用于逻辑复杂的数据更新
const [a,setA] = useState()
复制代码
const [state, dispatch] = useReducer(reducer, initialState);
复制代码
累加器
使用useState
实现
const [num, setNum] = useState(0)
function addNum() {
setNum(num + 1)
}
return (
<>
<p>{num}</p>
<button onClick={addNum}>+</button>
</>
)
复制代码
使用useReducer
实现
const initialNum = 0
const [num, dispatch] = useReducer(addNum, initialNum)
function addNum(state, action) {
console.log(state, action)
return state + 1
}
return (
<>
<p>{num}</p>
<button onClick={dispatch}>+</button>
</>
)
复制代码
双向绑定
使用useState
实现
const [text, setText] = useState("higher~oh~higher")
function changeText(value) {
setText(value)
}
return (
<>
<p>{text}</p>
<input type="text" onChange={(e) => changeText(e.target.value)} />
</>
)
复制代码
使用useReducer
实现
const initialText = "higher~oh~higher"
const [text, setText] = useReducer(changeText, initialText)
function changeText(_, value) {
return value
}
return (
<>
<p>{text}</p>
<input type="text" onChange={(e) => setText(e.target.value)} />
</>
)
复制代码
useEffect
在特定时间段进行操作,比如:请求初始数据、销毁数据、监听数据更新
模拟数据请求
类似于vue在组件加载时,请求初始数据。
const [data, setData] = useState([1, 2, 3])
const getData = () => {
return new Promise((res) => {
setTimeout(() => {
res([4, 5, 6])
}, 1000)
})
}
useEffect(() => {
getData().then((data) => {
setData(data)
})
}, [])
return (
<>
<ul>
{data.map((i) => {
return <li key={i}>{i}</li>
})}
</ul>
</>
)
复制代码
监听数据更新
useEffect(()=>{
...
},[data])
复制代码
模拟销毁数据
useEffect(()=>{
return ()=>{
//在销毁前做点什么
}
})
复制代码
useContext
可以从这个hook调用上下文空间,从而实现组件传值
createContext
使用这个api可以创建上下文空间。
createContext(默认值)
Provider:传递数据的一方
Consume:使用数据的一方
复制代码
const NumContext = createContext()
return <NumContext.Provider value={{num,setNum}}>
<Child />
</NumContext.Provider>
function Child(){
return <NumberContext.Consumer>
{({num}) => {
return <>{num}</>
}}
</NumberContext.Consumer>
}
复制代码
组件间传值
父子传值(或者说只有一层)
父->子
return <Child num={123} />
function Child(props){
//可以获取到props.num
}
复制代码
子->父:父组件需要给子组件传递一个函数名,然后在自己组件内部实现这个函数;子组件需要传值的时候,触发这个函数即可。
const changeNum = ()=>{
//更改num或者顺便做点别的,当然如果只需要更改Num,也可以将setNum作为参数传递
}
return <Child changeNum={changeNum} />
function Child(props){
return <button onClick={()=>{props.changeNum()}}>change</button>
}
复制代码
上下文传值(多少层都可以)
const NumContext = createContext()
return <NumContext.Provider value={{num,setNum}}>
<Child />
</NumContext.Provider>
function Child(){
const {num,setNum} = useContext(NumContext)
return <>
{num}
</>
}
复制代码
useRef
获取某个组件或dom元素
一般在表单元素里,可以使用
<input value={text} onChange={(e)=>setText(e.target.value)} />
复制代码
来实现双向绑定。
但对于无法改变value的组件,想获取组件的一些属性,就可以使用这个hook
const el = useRef(null)
<input ref={el}/>
//可以通过el.current获取到这个dom元素
复制代码
useMemo与useCallback
避免子组件被强制更新消耗性能
memo
可以使用memo()把子组件包起来,只适用于纯静态组件
const Child = memo(()=>{
return <>Child</>
})
复制代码
useCallback
对于一些要和父组件交互的子组件,可以在memo的基础上,把操作转移到父级
const Child = memo((props)=>{
return <button onClick={()=>{props.changeNum()}}>click</button>
})
function App(){
const [num,setNum] = useState(0)
const changeNum = useCallback(()=>{setNum(num=>num+1)},[])
return <>
{num}
<Child />
</>
}
复制代码
useMemo
和上面差不多
const changeNum = useMemo(()=>{
return ()=>{
setNum(num=>num+1)
}
},[])
复制代码
React Redux
基于Redux,结合Context设计出来的状态管理器
store
一般会新建一个同名文件夹,用index.js
作为入口文件。
//src/store/index.js
import { createStore } from "redux"
const defaultState = {
num: 1,
}
const reducer = () => {
return defaultState
}
const store = createStore(reducer)
export default store
复制代码
Provider
一般给最顶层的组件套Provider
//src/index.js
import { Provider } from "react-redux"
import store from "./store"
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
)
复制代码
Connect
在组件中,需要获取数据的时候,需要在导出时使用connect,在组件内部实现state和dispatch,connect可以将state和dispatch都映射成props里的属性。
//src/App.jsx
import React from "react"
import { connect } from "react-redux"
function App(props) {
return <div>num:{props.num}</div>
}
const storeState = (state) => {
return {
num: state.num,
}
}
export default connect(storeState)(App)
复制代码
Consume
经过上面的步骤,就可以在组件内使用store的数据了
function App(props) {
return <div>num:{props.num}</div>
}
复制代码
更改数据
- 在reducer加上action参数,并根据action.type实现更改数据的操作。如果直接修改state,不会触发视图的更新。
const reducer = (state = defaultState, action) => {
let newState = JSON.parse(JSON.stringify(state))
if (action.type === "addNum") { //type多了的时候,可以改成switch
newState.num++
}
return newState
}
复制代码
- 在组件中,实现dispatch并放在connect的第二个参数里
const dispatchStore = (dispatch) => {
return {
addNum() {
let action = { type: "addNum" }
dispatch(action)
},
}
}
export default connect(storeState, dispatchStore)(App)
复制代码
- 在组件html内部,可通过props调用dispatch
function App(props) {
return (
<>
<div>num:{props.num}</div>
<button
onClick={() => {
props.addNum()
}}
>
+1
</button>
</>
)
}
复制代码
React - Router
router
新建一个文件夹存放路由组件,先来实现一个基本的路由。
import { BrowserRouter, Route, Routes } from "react-router-dom"
import App from "../App6"
import Home from "../pages/Home"
const BaseRouter = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
</Route>
</Routes>
</BrowserRouter>
)
}
export default BaseRouter
复制代码
想要Home组件在App组件内部渲染
<Route path="/" element={<App />}>
<Route path="/home" element={<Home />}></Route>
</Route>
复制代码
最顶层的组件入口需要修改为路由的形式
import Router from "./router/index.jsx"
ReactDOM.render(
<Router />,
document.getElementById("root")
)
复制代码
父组件App要设置Outlet展示子路由
import { Outlet } from "react-router-dom"
export default function App6() {
return (
<>
<Outlet />
</>
)
}
复制代码
跳转
完成上述设置后,启动项目,发现并没有展示home页面,需要手动在url后加上/home
才能显示。可以使用几个hook来实现。
useLocation
获取当前路由的信息,其中包括path值
const xx = useLocation()
console.log(xx)
复制代码
useNavigate
接收路由属性为参数,可进行路由跳转
const navigate = useNavigate()
console.log(navigate)
复制代码
根据这两个Hook,可以使用useLocation
获取当前路由的pathname,若为默认值,则使用useNavigate
跳转至home页面。
useEffect(() => {
//执行跳转逻辑
if (pathname === "/") {
navigate('/home')
}
}, [])
复制代码
这样就可以实现vue-router里面默认重定向的效果了
Link
使用Link组件也可以实现路由跳转
<Link to="/home">Home</Link>
//相当于
<a href="/home">Back to Home</a>
复制代码
传参
如果是多级URL,比如/student/10001/report
,则需要在路由里设置
<Route path="/student/:id" element={<Student/>}>
<Route path="/report" element={<Report/>}></Route>
</Route>
复制代码
在目标页使用useParams
获取参数
//Student.jsx
const {id} = useParams()
复制代码
如果是用查询字符串表示,比如/student?id='10001'/report
,则可以使用useSearchParams
const [searchParams, setSearchParams] = useSearchParams()
searchParams.get('id')
复制代码
如果是使用navigate跳转传参,则可以用useLocation获取state
navigate("/student", { state: { id: '10001' } })
//Student.jsx
const { state } = useLocation()
console.log(state.id)
复制代码
404
<Route path="*" element={<Error />}></Route>
复制代码
小demo
好的,你已经充分了解react hooks和redux以及路由了。那么根据所学的知识来实现一个小demo吧。
那么这里有一个简易购物车,购物车里的数据从redux获得,可以点击某一项进入到商品详情,点击删除按钮删除这个数据。
数据结构设计
先来解决购物车数据的格式
car:[
{//商品的简略信息
good_id:1,
good_name:'aaa',
good_price:100,
good_pic:url(),
count:2
}
]
goods:[
{//商品的详细信息
id:1,
name:'aaa',
price:100,
pic:url(),
desc:'这是一个什么什么'
}
]
复制代码
列表组件
展示的是car的数据。
图片引入时,需要使用import而不是直接写在src
import goodImg from "...."
<img src={goodImg}/>
复制代码
在这个阶段碰到一个问题,如果子组件与父组件的样式重名了,父组件会覆盖子组件的样式。react中不像vue可以在标签里加个scoped
就完事,但可以通过配置css module和css in js实现。如果样式关系没那么复杂,直接给两个组件设置不一样的样式名也行。
我在这个demo里使用了styled-components
的方式,实现如下
//外层组件套了Provider store={store}
<ul>
{props.car.map((i) => {
return (
<MyLi key={i.id} className="item">
<div className="item-info">
<p className="title">商品名称:{i.name}</p>
<p className="price">商品单价:{i.price}</p>
<p className="nums">商品数量:{i.count}</p>
<p className="totle">
商品总价:¥{i.count * i.price}
</p>
</div>
<div className="item-tools">
<Button>查看</Button>
<Button>删除</Button>
</div>
</MyLi>
)
})}
</ul>
复制代码
//assets/css/list.js
import styled from "styled-components"
export const MyLi = styled.li`
width: 300px;
display: flex;
justify-content: space-between;
align-items: center;
> div.item-info > .title {
font-weight: bold;
}
`
复制代码
详情组件
展示的是goods组件
需要通过good_id
来展示详情,所以把路由改成
<Route path="/detail/:id" element={<Detail />}></Route>
复制代码
这样通过/detail/1
就可以访问id为1的商品详情了。
详情组件实现如下:
function Detail(props) {
const { id } = useParams()
const [item] = props.goods.filter((i) => i.id === Number(id))
return (
<div>
<p>商品名称:{item.name}</p>
<p>商品单价:{item.price}</p>
<p>商品详情:{item.desc}</p>
</div>
)
}
复制代码
如果没有找到这个商品,可以显示“该商品已下架”的提示。这里用到类似v-if+v-else的判断
item ? (
<div>
<p>商品名称:{item.name}</p>
<p>商品单价:{item.price}</p>
<p>商品详情:{item.desc}</p>
<Link to="/home">Back to Home</Link>
</div>
) : (
<div>该商品已下架</div>
)
}
复制代码
跳转逻辑
根据我的数据结构,当点击car里的某一项时,可以获取到good_id,将这个作为参数传递到详情组件里。然后可以根据这个id查找goods获取商品信息。
需要在点击查看按钮时,附带id数据跳转
const navigate = useNavigate()
const toDetail = (id) => {
navigate(`/detail/${id}`)
}
复制代码
删除逻辑
购物车的数据是来源于store的,所以只要触发store的更改就可以了
在对store有多个操作的时候,可以用switch-case来匹配
const reducer = (state = defaultState, action) => {
let newState = JSON.parse(JSON.stringify(state))
const { type } = action
switch (type) {
case "addNum": {
newState.num++
break
}
case "deleteItem": {
newState.car = newState.car.filter((i) => i.id !== action.id)
break
}
default:
break
}
return newState
}
复制代码
demo完成后就是这样