五年使用vue2、vue3经验,我直接上手react
在前端这个行业不管vue
和react
都是要会的,它们都是很优秀的,在前端的影响力都很大,市面上基本都是在使用这两种框架。
说下现在框架的特性: 数据驱动更新
、生命周期
、组件化
、路由
、监听数据
、JSX
、组件传值(子传父、父传子、全局值交互)
配合编译器webpack、vite、rollup
等等。
没说错吧,不管是vue
、react
,又或者微信小程序都
是以上几种特性组合而成的。本文会结合vue
做对比的方式提出两者的差异点,如果你刚好会vue
,正在学习react
那么本篇文章应该会对你受益很大。如果你是资深使用react
的,可以指出我总结下错误的地方。
本篇所有的代码链接: 代码仓库
本篇所有的DEMO
在线预览链接:在线预览
DEMO截图:
路由篇
路由来说,咱们需要知道以下这些就够用了:
- 怎么定义路由
- 指定路由的界面
- 如何切换路由
- 如何获取路由的参数,以及获取参数
定义路由
如果你是使用官方推荐的React Router (v7)
生成的脚手架npx create-react-router@latest
,那么使用上就可以像vue
的配置式一样了如下:
ts
// routes.ts
import { type RouteConfig, index, route, layout, prefix } from "@react-router/dev/routes";
export default [
layout('./components/Layout.tsx', [
index("routes/home.tsx"),
route('user', './views/user.tsx'),
route('user/:name', './views/user/detail.tsx'),
]),
...prefix("concerts", [
index("./views/concerts/user.tsx"),
]),
] satisfies RouteConfig;
index
就是代表根路径,直接是个文件地址就行route
代表子路由,第一个参数是路由地址,第二个参数是路由组件地址layout
代表布局,第一个参数是布局的组件地址,第二个参数是各个路由
tsx
// Layout.tsx
import { Outlet } from "react-router";
const AppLayout = () => {
return (
<div className="flex">
<div className="w-1/5">
<App />
</div>
<div className="w-4/5">
<Outlet />
</div>
</div>
);
};
使用Outlet
代表路由放的位置。有点类似于Vue
的<router-view />
的样子
prefix
很明显了就是前缀的意思,类似于vue的baseUrl
以上定义的直接通过运行的地址访问到了
bash
http://localhost:5173/
http://localhost:5173/user
http://localhost:5173/user/3367
http://localhost:5173/concerts
使用从零构建一个react
项目的npm create vite@latest my-app -- --template react
可以通过安装react-router
的方式去使用如下:
安装react-router
csharp
yarn add react-router
在你从零构建一个react
项目中main.jsx
中如下:
jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import About from './views/About.jsx'
import AppLayout from './components/layout/index.jsx'
import Page1 from './views/Page1.jsx'
import Page2 from './views/Page2.jsx'
import ConcertsIndex from './views/concerts/Index.jsx'
import City from './views/concerts/city.jsx'
import Trending from './views/concerts/trending.jsx'
import { BrowserRouter, Routes, Route } from "react-router";
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="about" element={<About />} />
{/* layout */}
<Route element={<AppLayout />}>
<Route path="page1" element={<Page1 />} />
<Route path="page2" element={<Page2 />} />
</Route>
{/* prefix */}
<Route path="concerts">
<Route index element={<ConcertsIndex />} />
<Route path=":city" element={<City />} />
<Route path="trending" element={<Trending />} />
</Route>
</Routes>
</BrowserRouter>
</StrictMode>
)
使用<BrowserRouter /> 、<Routes />、<Route />
来配合使用就行了基本跟上面一样,定义路由名称path
,指定组件element
。
可以使用react-router
中的<BrowserRouter /> 、<HashRouter />
来切换路由的hash、history
的模式
BrowserRouter
是 history
模式, 即: http://localhost:5173/home
jsx
// BrowserRouter 是 history 模式
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { BrowserRouter, Routes, Route } from "react-router";
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="home" element={<App />} />
</Routes>
</BrowserRouter>
</StrictMode>
)
HashRouter
是 hash
模式, 即: http://localhost:5173/#/home
jsx
// BrowserRouter 是 hash 模式
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { HashRouter, Routes, Route } from "react-router";
createRoot(document.getElementById('root')).render(
<StrictMode>
<HashRouter>
<Routes>
<Route path="home" element={<App />} />
</Routes>
</HashRouter>
</StrictMode>
)
这里在提下路由懒加载吧,react
中提供了lazy
和Suspense
组件来支持路由懒加载,使用起来非常方便。如下包裹:
jsx
import { StrictMode, lazy, Suspense } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import About from './views/About.jsx'
import AppLayout from './components/layout/index.jsx'
import Page1 from './views/Page1.jsx'
import Page2 from './views/Page2.jsx'
import ConcertsIndex from './views/concerts/Index.jsx'
import City from './views/concerts/city.jsx'
import Trending from './views/concerts/trending.jsx'
const LazyAbout = lazy(() => import('./views/About.jsx'))
import { BrowserRouter, Routes, Route } from "react-router";
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<App />} />
<Route path="about" element={<About />} />
{/* layout 布局*/}
<Route element={<AppLayout />}>
<Route path="page1" element={<Page1 />} />
<Route path="page2" element={<Page2 />} />
</Route>
{/* prefix 路由前缀 */}
<Route path="concerts">
<Route index element={<ConcertsIndex />} />
<Route path=":city" element={<City />} />
<Route path="trending" element={<Trending />} />
</Route>
{/* lazy 路由懒加载*/}
<Route path="lazy-about" element={<LazyAbout />} />
</Routes>
</Suspense>
</BrowserRouter>
</StrictMode>
)
切换路由
在react-router
中,切换路由可以使用useNavigate
jsx
// nav/index.jsx
import React from "react";
import { useNavigate } from 'react-router';
import { Button, Flex } from 'antd';
const NavIndex = () => {
const navigate = useNavigate();
const onClick1 = () => {
navigate('/nav/page1');
}
const onClick2 = () => {
navigate('/nav/page2');
}
const onClick3 = () => {
navigate('/nav/123');
}
return (
<>
<Flex gap="small" wrap>
<Button type="primary" onClick={onClick1}>去到 /nav/page1页面</Button>
<Button type="primary" onClick={onClick2}>去到 /nav/page2页面</Button>
<Button type="primary" onClick={onClick3}>去到 /nav/:id 页面</Button>
</Flex>
</>
);
};
export default NavIndex;
main.jsx
中增加
jsx
import { StrictMode, lazy, Suspense } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import About from './views/About.jsx'
import AppLayout from './components/layout/index.jsx'
import Page1 from './views/Page1.jsx'
import Page2 from './views/Page2.jsx'
import ConcertsIndex from './views/concerts/Index.jsx'
import City from './views/concerts/city.jsx'
import Trending from './views/concerts/trending.jsx'
import NavIndex from './views/nav/Index.jsx'
import NavPage1 from './views/nav/Page1.jsx'
import NavPage2 from './views/nav/Page2.jsx'
import NavPage3 from './views/nav/Page3.jsx'
const LazyAbout = lazy(() => import('./views/About.jsx'))
import { BrowserRouter, Routes, Route } from "react-router";
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<App />} />
<Route path="about" element={<About />} />
{/* layout */}
<Route element={<AppLayout />}>
<Route path="page1" element={<Page1 />} />
<Route path="page2" element={<Page2 />} />
</Route>
{/* prefix */}
<Route path="concerts">
<Route index element={<ConcertsIndex />} />
<Route path=":city" element={<City />} />
<Route path="trending" element={<Trending />} />
</Route>
<Route path="lazy-about" element={<LazyAbout />} />
{/* 路由切换相关代码 */}
<Route path="nav">
<Route index element={<NavIndex />} />
<Route path='page1' element={<NavPage1 />} />
<Route path='page2' element={<NavPage2 />} />
<Route path=':id' element={<NavPage3 />} />
</Route>
</Routes>
</Suspense>
</BrowserRouter>
</StrictMode>
)
navigate('/nav/page1');
做为路由跳转,navigate(-1);
可以做为路由返回。
路由参数传参获取
路由传参数有以下几种方式:
- 直接拼接到路由上
navigate('/nav/page1?id=1&name=zhangsan');
- 使用
navigate state
参数传递更复杂的数据(例如对象或数组)。这些数据不会出现在 URL 中,但可以在目标页面中访问,有点像是全局存储了一份数据。 - 路由
params
传参数 使用useParams
接收
第一种:navigate('/nav/page1?id=1&name=zhangsan');
方式使用react-router
中的useSearchParams
获取 如下:
jsx
// nav/page1.jsx
import React from "react";
import { useNavigate, useSearchParams } from 'react-router';
import { Button } from 'antd';
const Page1 = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const onBack = () => {
navigate(-1)
}
console.log(searchParams.get('id'), searchParams.get('name'), 'id,name'); // 输出: 1 zhangsan id,name
return (
<>
<div>
<h1>Nav Page1</h1>
<Button type="primary" onClick={onBack}>返回</Button>
</div>
</>
);
};
export default Page1;
第二种:navigate state
参数传递更复杂的数据如下,这种方式是利用history、hash
API做数据存储的都是存在你浏览器记录里面了,所以不能把链接分享给别人去使用
jsx
const onClick2 = () => {
navigate('/nav/page2', {
state: {
id: 2,
name: 'lisi'
}
});
}
获取方式如下:
jsx
// nav/page2.jsx
import React from "react";
import { useNavigate, useLocation } from 'react-router';
import { Button } from 'antd';
const Page2 = () => {
const navigate = useNavigate();
const location = useLocation();
const state = location.state || {};
console.log(state, 'state'); // 输出:{ "id": 2, "name": "lisi" } 'state'
const onBack = () => {
navigate(-1)
}
return (
<>
<div>
<h1>Nav Page2</h1>
<Button type="primary" onClick={onBack}>返回</Button>
</div>
</>
);
};
export default Page2;
第三种:路由params
传参数
jsx
const onClick3 = () => {
navigate('/nav/123');
}
jsx
import React from "react";
import { useNavigate, useParams } from 'react-router';
import { Button } from 'antd';
const Page3 = () => {
const navigate = useNavigate();
const {id} = useParams();
console.log(id, 'id'); // 输出:123 id
const onBack = () => {
navigate(-1)
}
return (
<>
<div>
<h1>Nav Page3</h1>
<Button type="primary" onClick={onBack}>返回</Button>
</div>
</>
);
};
export default Page3;
路由篇总结
其实看到这里也算是完全掌握路由相关的使用了,咱们接着往下看
组件编写以及JSX使用
组件的模块化,一处编写,多处使用。咱们接下来先编写一个简单的react
组件
jsx
// start/Index.jsx
import React from "react";
export default function Start() {
return (
<div>
<h1>About1</h1>
<h1>About2</h1>
</div>
);
}
Fragment
组件使用
不想要父元素(去掉一层元素)div
包裹可以使用Fragment
组件,如下:
jsx
import React, { Fragment } from "react";
export default function Start() {
return (
<Fragment>
<h1>About</h1>
<h1>About2</h1>
</Fragment>
);
}
Fragment
可以简写如下:
jsx
import React from "react";
export default function Start() {
return (
<>
<h1>About</h1>
<h1>About2</h1>
</>
);
}
JSX 基础使用方式
jsx
import React from "react";
export default function Start() {
const data = [
{
name: 1,
id: 1
},
{
name: 2,
id: 2
},
]
const currentType = true;
const onBtnClick = () => {
console.log('我是按钮点击了click')
}
return (
<>
<h1>About</h1>
<h1>About2</h1>
{/* 循环渲染数据 */}
<ul>
{
data.map((item) => (<li key={item.id}>{item.name}</li>))
}
</ul>
<ul>
{
data.filter((item) => item.name !== 1).map((item) => <li key={item.id}>{item.name}</li>)
}
</ul>
{/* 三目运算 */}
<div>
{ currentType ? <h1>true 111111</h1> : <h1>false22222</h1>}
</div>
{/* 添加class类名 */}
<div></div>
{/* 行内样式 */}
<div style={{color: 'red', fontSize: '16px'}}>我是什么颜色</div>
{/* 事件绑定 click*/}
<button onClick={onBtnClick}>我是按钮</button>
</>
);
}
父子组件传参数
其实在react
中省去了子传父 Vue 中 emit
事件了,直接利用props
传一个函数方法作为回调去做的,如下:
jsx
// parent.jsx
import React, { useState } from 'react';
import Children from './children';
const Parent = () => {
const [name, setName] = useState('张三');
return (
<div>
<h1>About Page</h1>
<Children name={name} onClick={setName}/>
</div>
);
};
export default Parent;
// children.jsx
import React from 'react';
const Children = ({ name, onClick}) => {
return (
<div>
<h1>{ name }</h1>
<button onClick={() => {onClick('李四')}}> 点击触发【子传父】</button>
</div>
);
};
export default Children;
以上就是一个简单的组件传值交互,就是把name
给子组件去使用,并且把函数setName
传了下去,做为点击事件的回调函数。
第二种方案是向下透传,类似于vue
的 Prvider、inject
,使用useContext、createContext
第一步创建index.js
js
import { createContext } from 'react'
// 创建上下文
export const DataContext = createContext(null)
第二步使用这个上下文方法
jsx
// index.jsx
import React, { useState } from "react";
import { DataContext } from './index'
import Children from "./children";
const ContextPage = () => {
const [name, setName] = useState('张三');
return (
// 注意这里是使用的 DataContext.Provider 并且必须是value字段,value 可以是字符串,对象,数组等
<DataContext.Provider value={{ name, setName }}>
<Children />
</DataContext.Provider>
);
};
export default ContextPage;
//children.jsx
import React, { useContext } from 'react';
import { DataContext } from './index'
const Children = () => {
// useContext 方法入参数就是前面定义好的上文DataContext,context接收后可以直接使用就行
const context = useContext(DataContext);
return (
<div>
<h1>{ context.name }</h1>
<button onClick={() => {context.setName('李四')}}> 点击触发【子传父】</button>
</div>
);
};
export default Children;
useContext、DataContext
是透传数据,不止父子组件,只要是父级<DataContext.Provider>
包裹内以下任何一级使用,上面咱们传输了一个对象name, setName
然后就可以给子级组件去使用了,注意绑定在<DataContext.Provider>
上的一定是value
生命周期
在使用react hooks
中省去生命周期,可以直接用useState、useEffect
去代替。这里先不去着重讲这几个hooks
的用法,请继续往下看
useState
代表初始化状态 类似于Vue
的onCreated
,其实在VUE
组合式API
中生命周期也基本可以省去了。useEffect
代表Vue中的onMounted、onDestroyed
事件
简单实用如下:
jsx
export const Demo = () => {
console.log('组件初始化');
const [count, setCount] = useState(0);
useEffect(() => {
console.log('组件挂载了');
// 比如使用了window.addEventListener
return () => {
console.log('组件卸载了');
// 比如使用了window.addEventListener 这里可以使用window.removeEventListener清楚掉
}
})
}
插槽
在react
对组件位置的存放更简单,不用定义固定的位置(<slot />
)或者说具名插槽(<slot name="footer" />
),可以直接使用如下:
先创建layout.jsx
文件:
jsx
import React from "react";
export default function Layout({ children, headerComponent }) {
return (
<div>
<header>
{ headerComponent }
</header>
<main>{children}</main>
</div>
);
}
再创建一个header.jsx
文件以及main.jsx
文件如下:
jsx
// header.jsx`
import React from "react";
const Header = () => {
return (
<div>我是头部Header部分代码</div>
)
}
export default Header
//main.jsx
import React from "react";
const Main = () => {
return (
<div>我是主体Main部分</div>
)
}
export default Main
接下来直接创建index.jsx
文件展示用法
jsx
import React from "react";
import Main from "./main";
import Header from "./header";
import Layout from "./layout";
const SoltPage = () => {
return (
<Layout headerComponent={<Header />}>
<Main>
</Main>
</Layout>
);
};
export default SoltPage
Layout
组件中的参数children
部分就是插槽
的默认部分,向Layout
直接传了一个<Header/>
,然后在Layout
组件就直接用了{headerComponent}
,也挺省事的。
react 样式编写
react
中样式使用css、less、scss
跟在vue中一样,直接下载依赖,然后直接创建对应的.less、.scss、.css
文件以import
引入到对应的组件即可,比如import './index.css'; import './index.less'; import './index.scss'
但是以上没有scope
组件样式隔离的效果,假如类名一样等情况会相互影响的。
在react
经常使用的样式隔离方案就是使用styled-components插件,这里咱们简单过下如何使用 ,如果对原理有兴趣可以参考我之前《为什么在vue中style-components没有火起来?》文章
下载插件:yarn add styled-components
, 如果是vscode
编辑器可以下载插件vscode-styled-components
,然后就能像写css样有代码提示了 使用如下:
jsx
import React from "react";
import styled from "styled-components";
// 写完就是一个组件,可以直接使用
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
min-height: calc(100vh - 40px);
`;
const StylePage = () => {
return (
<Container>
<div>
<h1>StylePage</h1>
</div>
</Container>
)
}
export default StylePage;
还可以直接更改子元素的样式如下:
jsx
import React from "react";
import styled from "styled-components";
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
min-height: calc(100vh - 40px);
h1 {
color: red;
}
`;
const StylePage = () => {
return (
<Container>
<div>
<h1>StylePage</h1>
</div>
</Container>
)
}
export default StylePage;
还可以进行props传参数使用如下:
jsx
import React from "react";
import styled from "styled-components";
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
min-height: calc(100vh - 40px);
h1 {
color: red;
}
`;
const Button = styled.button`
background-color: #007bff;
color: ${(props) => props.color};
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
`
const StylePage = () => {
return (
<Container>
<div>
<h1>StylePage</h1>
<Button color="white">Click Me</Button>
<Button color="red">Click Me</Button>
<Button color="blue">Click Me</Button>
</div>
</Container>
)
}
export default StylePage;
还可以跟组件一样嵌套使用如下:
jsx
import React from "react";
import styled from "styled-components";
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
min-height: calc(100vh - 40px);
h1 {
color: red;
}
`;
const Button = styled.button`
background-color: #007bff;
color: ${(props) => props.color};
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
`
const HoverButton = styled(Button)`
transition: background-color 0.3s ease;
&:hover {
background-color: #0056b3;
}
`
const StylePage = () => {
return (
<Container>
<div>
<h1>StylePage</h1>
<Button color="white">Click Me</Button>
<Button color="red">Click Me</Button>
<Button color="blue">Click Me</Button>
<HoverButton color="white">Click Me</HoverButton>
<HoverButton color="red">Click Me</HoverButton>
<HoverButton color="blue">Click Me</HoverButton>
</div>
</Container>
)
}
export default StylePage;
以上就是
styled-components
插件的使用,还有更改用法更多请看官方文档, 还有使用tailwindcss
的方式,大致就是定义好了类名,直接去使用就行(个人不是很喜欢这种)。
Hooks 核心应用
以下内置的几个hooks
是重重之重 每个都要必须熟练 掌握 useState
、useEffect
、useContext
、useReducer
、useCallback
、useMemo
、useRef
,我接下来会针对每个的使用场景 以及注意事项用法都仔细的讲一遍
useState
useState
是用于在函数组件中管理组件的状态,它返回一个状态值和一个更新状态的函数。使用如下:
以下就是一个简单的例子,点击按钮+1
jsx
import React from "react";
import { Button, Divider } from "antd";
const UseStatePage = () => {
const [count, setCount] = React.useState(0);
return (
<>
<div>
<Button onClick={() => setCount(count + 1)}>点击+1</Button>
<h1>{count}</h1>
</div>
<Divider />
</>
);
};
export default UseStatePage;
再来看一个案例点击按钮如果触发两次setCount2(count2 + 1)
会怎么样?
jsx
import React from "react";
import { Button, Divider } from "antd";
const UseStatePage = () => {
const [count, setCount] = React.useState(0);
const [count2, setCount2] = React.useState(0);
return (
<>
<div>
<Button onClick={() => setCount(count + 1)}>点击+1</Button>
<h1>{count}</h1>
</div>
<Divider />
<div>
<h1>陷阱:错误❌的使用示例</h1>
<Button
onClick={() => {
setCount2(count2 + 1)
setCount2(count2 + 1)
}}
>
按钮2: 点击+1+1
</Button>
<h1>{count2}</h1>
</div>
</>
);
};
export default UseStatePage;
以上每次点击
按钮2: 点击+1+1
还是每次+1 ,不会按照预期+2
,因为react
的执行时机是异步的。请再接着往下看
jsx
// 加一个定时器
const [count3, setCount3] = React.useState(0);
<div>
<h1>陷阱:错误❌的使用示例</h1>
<Button
onClick={() => {
setCount3(count3 + 1)
setTimeout(() => {
setCount3(count3 + 1)
}, 500)
}}
>
点击+1+1
</Button>
<h1>{count3}</h1>
</div>
加上定时器也是一样的不会每次加2,官方的解释是:这是因为 状态表现为就像一个快照。更新状态会使用新的状态值请求另一个渲染,但并不影响在你已经运行的事件处理函数中的 count JavaScript 变量。那么如何解决这个问题呢?
传入一个函数即可:(请注意a 更改是函数的入参数,不是原来的count4)
jsx
const [count4, setCount4] = React.useState(0);
<div>
<h1>正确✅使用示例</h1>
<Button
onClick={() => {
setCount4((a) => a + 1)
setCount4((a) => a + 1)
}}
>
点击+1+1
</Button>
<h1>{count4}</h1>
</div>
Object
类型要如何更改数据呢?请看以下示例
jsx
import React, { useState } from "react";
import { Button } from "antd";
const UseStateOther = () => {
const [count, setCount] = useState({
name: 'zhangsan',
age: 18
});
return (
<>
<h1> Object 类型使用示例</h1>
<Button onClick={() => {
setCount({
...count,
age: count.age + 1
})
}}>
age + 1
</Button>
<pre>{JSON.stringify(count,null,2)}</pre>
</>
)
}
export default UseStateOther;
setCount({...count, age: count.age + 1 })
,就是把原来的参数使用...
,然后再去更改age
的值,那么有没有更方便的更改方式呢?能直接更改就生效了呢?
如下咱们简单写个useImmer
方法用来便捷的更改Object
类型,使之能直接使用count.age++
来更新
jsx
const useImmer = (initState) => {
const [state, setState] = useState(initState);
const setStateImmer = (callback) => {
const data = typeof state === 'object' ? { ...state } : state
if(typeof callback === 'function') {
callback(data)
}
setState(data)
}
return [state, setStateImmer];
};
大致就是对useState
的setData
包裹了一层方法用来处理Object
等类型的更新,使用如下:
jsx
const [data2, setData2] = useImmer({
name: 'zhangsan',
age: 18
});
<div>
<h1> Object 简单使用示例</h1>
<Button onClick={() => {
setData2((draft) => {
draft.age++
})
}}>
age + 1
</Button>
<pre>{JSON.stringify(data2,null,2)}</pre>
</div>
<Divider />
上面只是简单的
useImmer
的函数封装,应该还有好多没有考虑到的情况,大家可以直接使用官方推荐的immerjs/use-immer 依赖,直接安装去使用就行了。
接下来咱们对数组
的数据处理方式也做个简单介绍,对数组的增加、删除、改,我下面就直接使用use-immer
依赖宝使用了,如下:
jsx
import React from "react";
import { Button, Divider } from "antd";
import { useImmer } from 'use-immer'
const UseStateArray = () => {
const [data, setData] = useImmer([
{
name: 'zhangsan',
age: 18
}
]);
return (
<>
<div>
<h1>数组 增加</h1>
<Button onClick={() => {
setData((draft) => {
draft.push({
name: 'lisi',
age: 18
})
})
}}>
加一条数据
</Button>
<pre>{JSON.stringify(data,null,2)}</pre>
</div>
<Divider />
<div>
<h1>数组删除最后一条数据</h1>
<Button onClick={() => {
setData((draft) => {
draft.splice(-1, 1)
})
}}>
减一条数据
</Button>
<pre>{JSON.stringify(data,null,2)}</pre>
</div>
<Divider />
<div>
<h1>数组对第一条数据age + 1</h1>
<Button onClick={() => {
setData((draft) => {
draft[0].age++
})
}}>
age++
</Button>
<pre>{JSON.stringify(data,null,2)}</pre>
</div>
</>
)
}
export default UseStateArray;
useState
总结: 如果更改基本类型可以直接使用useState
,如果需要更改数组、对象
等更便捷可以使用useImmer
方法。
useEffect
注意再开发模式中由于react
的严格机制,useEffect
会被更新2次 ,主要是怕你使用不当导致出现的bug
。
useEffect
第二个字段为空数组时,可以当作onMounted
去使用,只在首次触发,不做任何监听。useEffect
第二个字段不为空数组时,可以当作onUpdated
去使用,监听到该数组内数据变化,会触发。useEffect
第二个字段不传时 ,只要该组件被reload
就会被触发(该组件任意值更新)。useEffect
第一个参数return
一个函数,可以当作onUnmounted
去使用。
useEffect
可以把它当作onMounted、onUnmounted、onUpdated、
去使用。使用如下:
jsx
// indx.jsx
import React, { useState, useEffect } from "react";
import { Button, Divider } from 'antd'
import Children from "./children";
const UseEffectPage = () => {
const [count, setCount] = useState(0);
const [data, setData] = useState(0);
useEffect(() => {
setData((a) => a + 1)
}, [])
return (
<>
<h1>基本使用</h1>
<Button onClick={() => setCount(count + 1)}>点击更改值</Button>
<h3>{count}</h3>
<h2>useEffect的触发次数: {data}</h2>
<Children count={count} />
<Divider />
</>
)
};
export default UseEffectPage;
// children.jsx
import React, { useEffect } from "react";
const Children = ({count}) => {
const [data, setData] = React.useState(0);
useEffect(() => {
setData((a) => a + 1)
}, []);
return (
<div>
<h1>{count}</h1>
<h2>useEffect子组件更新的次数{data}</h2>
</div>
);
};
export default Children;
在点击按钮时,由于useEffect
为空,不做任何监听,不会产生触发。
所以在useEffect
第二个参数为空 时,就可以单纯当作onMounted
去使用。
useEffect
如果监听count
,在点击按钮时就可以看到更新了(每次加1),如下:
jsx
useEffect(() => {
setData((a) => a + 1)
}, [count])
如果第二个参数直接不填会怎么样?如下:
jsx
// children.jsx
import React, { useEffect } from "react";
const Children = ({count}) => {
const [data, setData] = React.useState(0);
useEffect(() => {
// setData((a) => a + 1)
console.log('子组件更新了')
});
return (
<div>
<h1>{count}</h1>
<h2>useEffect子组件更新的次数{data}</h2>
</div>
);
};
export default Children;
其实会看到每次父组件更改count
值后,子组件进行reload
,由于useEffect
第二个参数没传,导致跟子组件一样,每次触发2次。
useEffect
第一个参数return
一个函数,可以当作onUnmounted
去使用,示例如下
jsx
// children.jsx
import React, { useEffect } from "react";
const Children = ({count}) => {
const [data, setData] = React.useState(0);
useEffect(() => {
setData((a) => a + 1)
return () => {
console.log("useEffect子组件卸载");
};
}, []);
return (
<div>
<h1>{count}</h1>
<h2>useEffect子组件更新的次数{data}</h2>
</div>
);
};
export default Children;
请再看下面一个示例:
jsx
// index.jsx
import React, { useState, useEffect } from "react";
import { Button, Divider } from 'antd'
import Children from "./children";
const UseEffectPage = () => {
const [count, setCount] = useState(0);
const [data, setData] = useState(0);
console.log('父组件更新了');
useEffect(() => {
setData((a) => a + 1)
}, [count])
return (
<>
<h1>基本使用</h1>
<Button onClick={() => setCount(count + 1)}>点击更改值</Button>
<h3>{count}</h3>
<h2>useEffect的触发次数: {data}</h2>
<Divider />
<Children />
<Divider />
</>
)
};
export default UseEffectPage;
// children.jsx
import React, { useEffect } from "react";
import { Button, Divider } from 'antd'
const Children = () => {
const [data, setData] = React.useState(0);
const [count1, setCount] = React.useState(0);
console.log('子组件更新了');
useEffect(() => {
setData((a) => a + 1)
return () => {
console.log("useEffect子组件卸载");
};
}, []);
return (
<div>
<h2>useEffect子组件更新的次数{data}</h2>
<Divider />
<Button onClick={() => setCount(count1 + 1)}>子组件+1</Button>
<h3>子组件{count1}</h3>
</div>
);
};
export default Children;
可以把关注点看在两个console.log('父组件更新了');、console.log('子组件更新了');
,在点击了父子 组件的按钮,会怎样执行,在这里讲下react
的更新机制
- 父组件中有字段更新更新,就会触发父组件以及子组件(没用props字段也会)
reload
。 - 子组件字段有更新,当前子组件会被重
reload
,父组件不会从新reload
那么有没有办法解决父组件更新,子组件不用reload
呢?也是有的哈可以使用mome
函数包裹子组件一层。如下:
jsx
import React, { useEffect, memo } from "react";
import { Button, Divider } from 'antd'
const Children = memo(() => {
const [data, setData] = React.useState(0);
const [count1, setCount] = React.useState(0);
console.log('子组件更新了');
useEffect(() => {
setData((a) => a + 1)
return () => {
console.log("useEffect子组件卸载");
};
}, []);
return (
<div>
<h2>useEffect子组件更新的次数{data}</h2>
<Divider />
<Button onClick={() => setCount(count1 + 1)}>子组件+1</Button>
<h3>子组件{count1}</h3>
</div>
);
});
export default Children;
这个时候父组件有更新,子组件就不会更新了。除非是子组件上有参数更新了,子组件才会更新,比如:
jsx
// index.jsx
import React, { useState, useEffect } from "react";
import { Button, Divider } from 'antd'
import Children from "./children";
const UseEffectPage = () => {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(0);
const [data, setData] = useState(0);
console.log('父组件更新了');
useEffect(() => {
setData((a) => a + 1)
}, [])
return (
<>
<h1>基本使用</h1>
<Button onClick={() => setCount(count + 1)}>点击更改值count</Button>
<h3>{count}</h3>
<Button onClick={() => setCount1(count1 + 1)}>点击更改值count1</Button>
<h3>{count1}</h3>
<h2>useEffect的触发次数: {data}</h2>
<Divider />
<Children count={count} />
<Divider />
</>
)
};
export default UseEffectPage;
// children.jsx
//这里跟上面一样,请注意 我把count绑定在子组件上
在点击点击更改值coun 后发现父子组件都做reload
了。 在点击点击更改值count1 后就父组件reload,子组件不会。
**总结:**关于reload
的组件更新机制刚才也说了,如何利用mome
优化也有使用示例,useEffect
的第二个参数不传、为空数组、为有数据相关都有说明示例。
useRef
注意这里跟vue
的ref
超级不一样 ,react
中改变 ref
不会触发重新渲染 ,所以 ref 不适合用于存储期望显示在屏幕上的信息。如有需要,使用useState
代替。
写个错误示例: 这里的count
值会一直为0,因为useRef
的current
值不会触发reload
。
jsx
// index.jsx
import React, { useRef, useEffect } from "react";
import { Button } from "antd";
const useRefPage = () => {
const count = useRef(0);
console.log('组件重新渲染!!!')
useEffect(() => {
console.log(count.current)
}, [count])
return (
<div>
<h1>错误❌示例</h1>
<Button onClick={() => {
count.current++
}}>
Ref点击加加
</Button>
<h1>{count.current}</h1>
</div>
)
}
export default useRefPage
useRef
更适合用来绑定dom
,存储定时器, 存储信息数据,比如:
jsx
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
开始
</button>
<button onClick={handleStop}>
停止
</button>
</>
);
}
以上示例用useRef
来存储定时器,useRef
的current
值不会触发reload
。并且在组件重新渲染(更新state)时,useRef
的current
值不会改变。
在或者用来操作DOM
,如下:
jsx
import React, { useRef, useEffect } from 'react';
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
// 页面加载完成后聚焦到输入框
inputRef.current.focus();
console.log(inputRef.current.value); // 获取输入框的值
}, []);
return <input ref={inputRef} />;
}
警告⚠️:useRef
不能像Vue
的ref
绑定在组件上 可以获取子组件的值。可以传输给子组件,交给子组件去绑定dom
,如下:
jsx
//index.jsx
const componentRef = useRef(null);
<DomRef ref={componentRef}/>
// Dom.jsx
import React, { useRef, useEffect } from 'react';
const DomRef = ({ref}) => {
const inputRef = useRef(null);
useEffect(() => {
// 页面加载完成后聚焦到输入框
inputRef.current.focus();
console.log(inputRef.current.value); // 获取输入框的值
}, []);
return (
<>
<h1>绑定DOM 示例</h1>
<input ref={inputRef} />
<a ref={ref}>我是A标签</a>
</>
);
}
export default DomRef;
请注意⚠️: 如果你是react v18以及以下版本
,需要用forwardRef
函数包裹下才能向下传输ref值,如下:(v19
该API已废弃可以像上面直接传输ref)
jsx
import { forwardRef } from 'react'
const DomRef = forwardRef(({ref}) => {
const inputRef = useRef(null);
useEffect(() => {
// 页面加载完成后聚焦到输入框
inputRef.current.focus();
console.log(inputRef.current.value); // 获取输入框的值
}, []);
return (
<>
<h1>绑定DOM 示例</h1>
<input ref={inputRef} />
<a ref={ref}>我是A标签</a>
</>
);
})
export default DomRef;
useReducer
参数描述 const [state, dispatch] = useReducer(reducer, initialArg, init?)
state
是当前状态值dispatch
是一个函数,用于触发状态更新,它接收一个参数action
,可以是一个对象或一个函数。reducer
是一个函数,它接收两个参数state
和action
,并根据action
的类型返回一个新的状态值。initialArg
是一个可选参数,用于初始化状态值,如果提供了init
函数,则initialArg
将作为init
函数的参数。
简单使用示例如下:
jsx
// index.jsx
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
...state,
age: state.age + 1
};
}
throw Error('Unknown action.');
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42, name: 'Joe' });
return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
Increment age
</button>
<p>Hello {start.name}! You are {state.age}.</p>
</>
);
}
还可以把dispatch
传给子组件去触发<Children dispatch={dispatch}/>
猛的一看不就useState
的setSate
放在了reducer
函数上了,其实也确实这个样,这样会让数据状态管理更加清晰,集中,方便维护。
咱们以useState、useReducer
分别实现一个对数据{ age: 42, name: 'Joe' }
的更改,可以做个对比如下:
jsx
// reducer.jsx
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
...state,
age: state.age + 1
};
}
if(action.type === 'decremented_age') {
return {
...state,
age: state.age -1
};
}
if(action.type === 'change_name') {
return {
...state,
name: '张三'
};
}
throw Error('Unknown action.');
}
export default function ReducerCounter() {
const [state, dispatch] = useReducer(reducer, { age: 42, name: 'John' });
return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
增加年龄
</button>
<button onClick={() => {
dispatch({ type: 'decremented_age' })
}}>
减少年龄
</button>
<button onClick={() => {
dispatch({ type: 'change_name' })
}}>
更改名字
</button>
<p>Hello{state.name}! You are {state.age}.</p>
</>
);
}
// state.jsx
import { useState } from 'react';
export default function StateCounter() {
const [data, setData] = useState({ age: 42, name: 'John' });
const incremented = () => {
setData({
...data,
age: data.age + 1
})
}
const decremented = () => {
setData({
...data,
age: data.age - 1
})
}
const change = () => {
setData({
...data,
name: '张三'
})
}
return (
<>
<button onClick={incremented}>
增加年龄
</button>
<button onClick={decremented}>
减少年龄
</button>
<button onClick={change}>
更改名字
</button>
<p>Hello{data.name}! You are {data.age}.</p>
</>
);
}
可以看出useReducer
其实把数据处理更加聚合,集中,清晰了。
use-immer
依赖包中也有useImmerReducer
可以直接使用,咱们把上面的useReducer
改成useImmerReducer
可以看到会更清晰一些,如下:
jsx
import React from 'react';
import { useImmerReducer } from 'use-immer'
function reducer(draft, action) {
switch (action.type) {
case 'incremented_age':
draft.age++
break;
case 'decremented_age':
draft.age--
break;
case 'change_name':
draft.name = '张三'
break;
default:
throw Error('Unknown action.');
}
}
export default function ImmerReducerCounter() {
const [state, dispatch] = useImmerReducer(reducer, { age: 42, name: 'John' });
return (
<>
<h1>useImmerReducer 管理</h1>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
增加年龄
</button>
<button onClick={() => {
dispatch({ type: 'decremented_age' })
}}>
减少年龄
</button>
<button onClick={() => {
dispatch({ type: 'change_name' })
}}>
更改名字
</button>
<p>Hello{state.name}! You are {state.age}.</p>
</>
);
}
注意事项:
-
reducer
方法中第一个值在没有使用useImmerReducer
是不可以更改的
-
- 请确定好
type
类型,避免使用**魔法字符串(incremented_age、decremented_age、change_name)**去判断,可以使用一个枚举类型,或者一个MAP
,减少类型错误的发生
- 请确定好
useContext
这个其实在父传子 哪里有说有具体用法,这里就不多说了可以直接在上面查看<DataContext.Provider value={{ name, setName }}>
useContext
可以搭配useReducer
做向下深度交互,咱们以前面的useReducer
示例用 useContext
来重新写下:
jsx
// index.js
import { createContext } from 'react'
export const DataContext = createContext()
// index.jsx
import React from "react";
import { DataContext } from "./index";
import { useImmerReducer } from 'use-immer'
import ViewComponent from './view'
import OperatorButton from "./operator";
function reducer(draft, action) {
switch (action.type) {
case 'incremented_age':
draft.age++
break;
case 'decremented_age':
draft.age--
break;
case 'change_name':
draft.name = '张三'
break;
default:
throw Error('Unknown action.');
}
}
const UseContextPage = () => {
const [state, dispatch] = useImmerReducer(reducer, { age: 42, name: 'John' });
return (
<DataContext.Provider
value={{
state,
dispatch
}}>
<OperatorButton />
<ViewComponent />
</DataContext.Provider>
);
};
export default UseContextPage;
// view.jsx
import React, { useContext } from "react";
import { DataContext } from './index'
const ViewComponent = () => {
const { state } = useContext(DataContext);
return (
<div>
<p>Hello{state.name}! You are {state.age}.</p>
</div>
);
};
export default ViewComponent;
// operator.jsx
import React, { useContext } from "react";
import { DataContext } from './index'
const OperatorButton = () => {
const { dispatch } = useContext(DataContext);
return (
<>
<h1>useContext + useReducer 管理</h1>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
增加年龄
</button>
<button onClick={() => {
dispatch({ type: 'decremented_age' })
}}>
减少年龄
</button>
<button onClick={() => {
dispatch({ type: 'change_name' })
}}>
更改名字
</button>
</>
)
};
export default OperatorButton;
以上只是一个简单使用
useContext
的示例,其实如果只是子组件需要使用上下文的数据直接通过props
传值就行,如果是在需要父组件透传数据的场景下用useContext
比较好,像主题切换、国际化等等场景下。
useMome
useMome
跟Vue
的computed
很是类似,不同点就是第二个参数需要写下const cachedValue = useMemo(calculateValue, dependencies)
calculateValue
要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。dependencies
所有在calculateValue
函数中使用的响应式变量组成的数组。
jsx
import React, { useState, useMemo} from "react";
const UseMomePage = () => {
const [count, setCount] = useState(1)
const [count1, setCount1] = useState(2)
const doubleCount = useMemo(() => {
return count * count1
}, [count, count1])
return (
<div>
<h1>useMemo 使用示例</h1>
<button onClick={() => setCount(count + 1)}>点击更改count+1</button>
<button onClick={() => setCount1(count1 + 1)}>点击更改count1+1</button>
<p>当前count值:{count}</p>
<p>当前count1值:{count1}</p>
<p>当前useMemo后doubleCount值:{doubleCount}</p>
</div>
)
}
export default UseMomePage
像前面的示例中如果第二个参数不仅仅传递[count]
,确实只能监听到count
更新才能从新计算了,但是代码中会有警告(React Hook usemo缺少一个依赖项:'count1'。要么包含它,要么删除依赖项) ,因为count1
没有被监听到。
注意为了保证calculateValue
是一个纯函数,react
在开发模式下会默认触发两次该函数,为你避免使用错误,及时发现bug
useCallback
useCallback
和useMemo
很像,不同点是useCallback
返回的是一个函数,useMemo
返回的是一个值。
在下面一种场景中使用useCallback
可以记忆一下setCount
避免重复更新子组件
jsx
import React, { useState, useCallback } from 'react';
// 子组件
const ChildComponent = React.memo(({ onIncrement }) => {
console.log('子组件重新渲染!!');
return (
<button onClick={onIncrement}>点击+1</button>
);
});
// 父组件
function ParentComponent() {
const [count, setCount] = useState(0);
// 使用 useCallback 包裹回调函数
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []); // 依赖数组为空,表示回调函数只在组件挂载时创建一次
return (
<div>
<p>Count: {count}</p>
<ChildComponent onIncrement={increment} />
</div>
);
}
export default ParentComponent;
useCallback
和useMemo
和mome函数
都是React
提供的用于优化性能的,合理的运用会有性能显著提升`
react
proxy 代理
在vue
中是直接更改vue.config.js
中的devServer.proxy
去配置代理的,其实内部也是使用的http-proxy-middleware
这个插件实现的,感兴趣可以去看看如何用代理http、https
服务的。
在react
中需要手动下载这个插件yarn add http-proxy-middleware
,然后在src
目录下创建setupProxy.js
用法跟vue
的一样。
js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000',
changeOrigin: true,
pathRewrite: {
'^/api': '' // 去掉请求路径中的 `/api` 前缀
}
})
);
app.use(
'/api2',
createProxyMiddleware({
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {
'^/api2': ''
}
})
);
};
React
使用TS
因为react
支持ts
确实比较好,大多数公司都会选择react + ts
开发,我觉得如果你都学到这里了ts
也可以简单学学,都是一些基础的东西。
在我刚写ts
的时候,领导告诉我说ts
是一种思想,你可以把所有的代码都有提示,鼠标放上去就知道怎么穿参数,以及对象引用都可以有代码提示,写代码不要太爽。
刚学初期只要保证不是用any
,保证每个代码都是有类型提示的就行,类型体操可以等你写熟练了后,其实就跟你写编程一样,把类型定义写的更加抽象了(这里建议适当,不要太抽象)
下一期我会单独出一篇ts
的相关应用,就不在这里多说了
总结
咱们从路由->组件JSX编写->组件传值->插槽->样式编写->常用Hooks的相关细节应用->代理
等各个方面做了详细的讲解。以上全部吸收后,写react
完全没有问腿, 希望会对你有所帮助。
本篇所有的代码链接: 代码仓库
本篇所有的DEMO
在线预览链接:在线预览
相关截图: