Vite+React+TS基础学习,看这里就够了!(下)
7. 受控组件和非受控组件
React中的表单组件(例如input,select等)可以分为两种类型:受控组件和非受控组件
。
7-1 受控组件
受控组件是指由React来管理组件的状态,以及通过事件处理函数将新的值通知给React。这意味着组件的值被存储在state中,并且只能通过setState()方法来更新它们。当用户输入内容时,React会将新的值更新到state中,并将其传递给组件的props,以便可以显示最新的值。
import React, { useState } from 'react';
function MyForm() {
// 我们定义了一个使用useState hook来管理输入内容的状态变量value。我们将文本框的value属性设置为value,onChange事件处理函数handleChange将使用setState方法更新value值。
// 此时,我们可以说这是一个受控组件,因为React管理了这个组件的状态,通过setState方法更新它,并在每次界面重新render时由value属性来保证文本框的值与组件的状态同步。
// setValue实际上是一个由useState hook生成的函数,用于更新状态变量的值
const [value, setValue] = useState('');
const handleChange = (event:React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
const handleSubmit = (event:React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
alert(`You entered: ${value}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={value} onChange={handleChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default MyForm;
7-2 非受控组件
非受控组件是指由DOM本身管理组件的状态。这些组件的值并不由React管理,而是直接从DOM获取。当用户输入内容时,组件的值立即被更新,而不需要等待React进行任何操作。
对于上面的例子,如果我们想改成非受控组件,可以直接从DOM中获取文本框的值并保存在变量中,然后在表单提交时使用该变量的值。这个例子可以写成这样:
function MyForm() {
let inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (event:React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
alert(`You entered: ${inputRef.current!.value}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
{/* 注意此处ref的赋值方式 */}
<input type="text" ref={inputRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default MyForm;
8. 关于react自带的优化手段
React 自带的优化手段包括 memo
、useMemo
、PureComponent
和 shouldComponentUpdate
。
memo
memo 是一个高阶组件,用于包装函数组件,它会对函数组件的输入进行浅层比较,如果输入没有变化,则直接返回上一次的结果,避免重新渲染组件。memo 适用于无状态组件或者组件只依赖于 props,没有依赖于 state 或 context 等外部因素的情况。
import React, { memo } from 'react';
// 当函数组件的渲染结果只依赖于 props 时,可以使用 memo 进行性能优化,包裹一个组件,缓存渲染结果
interface IComponentProps{
propA:number
propB:number
}
const MyComponent:React.FC<IComponentProps> = memo(({ propA, propB }) => {
// 根据 props 计算结果,并输出到页面上
return <div>{propA + propB}</div>;
});
export default MyComponent
useMemo
联系:useMemo的用法
useMemo 是 React 提供的一个 Hook,它也可以用于缓存计算结果,避免重复计算,但是和 memo 不同的是,useMemo 的缓存是在函数组件内部维护的,可以缓存任意类型的值,而不仅限于组件的渲染结果。
// memo 主要是用于缓存组件渲染的结果,而 useMemo 主要是用于缓存变量的值。
interface IComponentProps{
propA:number
propB:number
}
const MyComponent:React.FC<IComponentProps> = ({ propA, propB }) => {
const result = useMemo(() => {
return propA + propB;
}, [propA, propB]);
return <div>{result}</div>;
};
export default MyComponent
PureComponent
PureComponent 是一个继承自 React.Component 的类组件,它会默认实现 shouldComponentUpdate 方法,并在组件更新前比较新的 props 和 state 和旧的 props 和 state 是否有变化,如果没有变化,则不会触发重新渲染。使用 PureComponent 的前提条件是 props 必须是不可变的,即不能直接修改 props 中的值,否则 shouldComponentUpdate 可能会判断出变化而导致不必要的重新渲染。 PureComponent 适用于控制整个组件更新的情况。
// 缓存渲染结果,根据props 和 state是否更新来决定是否重新渲染
interface IComponentProps{
propA:number
propB:number
}
class MyComponent extends PureComponent<IComponentProps> {
//初始化状态
state = {isHot:false,wind:'微风'}
//自定义方法————要用赋值语句的形式+箭头函数
changeWeather = ()=>{
// 读取显示 ==>this.state.isHot
const isHot = this.state.isHot
// 更新状态-->更新界面 : this.setState({stateName1 : newValue})
this.setState({isHot:!isHot})
}
render() {
const {isHot,wind} = this.state
return
<div>
<h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
<div>{this.props.propA + this.props.propB}</div>
</div>
}
}
export default MyComponent
shouldComponentUpdate
shouldComponentUpdate 是一个生命周期方法,用于手动控制组件的重新渲染。当 shouldComponentUpdate 方法返回 false 时,组件不会重新渲染,否则会执行正常的渲染流程。shouldComponentUpdate 适用于需要根据具体场景手动控制组件更新的情况,比如性能优化、避免不必要的网络请求等。
// shouldComponentUpdate手动控制重新渲染
interface IComponentProps{
propA:number
propB:number
}
interface IComponentState{
stateA:boolean
stateB:string
}
class MyComponent extends Component<IComponentProps,IComponentState> {
//初始化状态
state = {stateA:false,stateB:'微风'}
shouldComponentUpdate(nextProps:IComponentProps, nextState:IComponentState) {
// 根据当前和下一次的 props 和 state 值,判断是否需要刷新组件
return nextProps.propA !== this.props.propA || nextState.stateA !== this.state.stateA;
}
render() {
// 输出 props 到页面上
return <div>{this.props.propA + this.props.propB}</div>;
}
}
export default MyComponent
9. jsx语法,变量绑定,条件渲染,列表渲染,事件处理
9-1 jsx语法
JSX 全称 JavaScript XML,是React 定义的一种类似于 XML 的 JS 扩展语法:JS + XML 本质是 React.createElement(component, props, ...children) 方法的语法糖。 用于简化创建虚拟 DOM,写法:var ele = <h1>Hello JSX!</h1>
。注意,它不是字符串,也不是 HTML/XML 标签。它最终产生的是一个 JS 对象。
优势:声明式语法更加直观,与 HTML 结构相同,降低了学习成本,提高了开发效率,JSX 是 React 的核心之一。
作用:用来创建react虚拟DOM(元素)对象,解决React.createElement() 创建 React 元素的问题:繁琐/不简洁;不直观,无法一眼看出所描述的结构;代码不容易维护。
🤔 JSX 并不是标准的 ECMAScript 语法,为什么 React 脚手架中可以直接使用 JSX 呢?
① JSX 需要使用 Babel 编译成 React.createElement() 的形式,然后配合 React 才能在浏览器中使用,而 create-react-app 脚手架中已经内置了 Babel 及相关配置。
② 编译 JSX 语法的包为:@babel/preset-react,在线体验。
9-1-1 定义虚拟 DOM 时,不要写引号
在React中使用JSX语法时,我们可以用类似HTML的标记来定义虚拟DOM。在这种情况下,如果你写了引号,那么它会被解释为一个字符串类型而不是一个JavaScript表达式。为了避免这种情况,我们可以直接使用JavaScript表达式,例如变量、函数、对象等,而不必使用引号来将其包裹。在 JSX 中,我们用大括号 {} 来表示需要动态计算的 JavaScript 表达式。
举个例子,如果我们要在JSX中渲染一个按钮,我们可以这样写:
const buttonText = 'Click me';
const handleClick = () => console.log('Button clicked!');
const Button = () => (
<button onClick={handleClick}>
{buttonText}
</button>
);
9-1-2 标签中混入 JS 表达式要用 {},注意JSX语法里面的注释写法
const myId = 'aTgUiGu'
const myData = 'Hello,rEaCt'
//创建虚拟DOM
const VDOM = (
{/* 注释 */}
<h2 id={myId.toLowerCase()}>
<span>{myData.toLowerCase()}</span>
</h2>
)
9-1-3 样式的类名指定不要用class,要用className
const myId = 'aTgUiGu'
const myData = 'Hello,rEaCt'
//创建虚拟DOM
const VDOM = (
<h2 className="title" id={myId.toLowerCase()}>
<span>{myData.toLowerCase()}</span>
</h2>
)
9-1-4 内联样式,要使用style={{key:value}}的样式
const myId = 'aTgUiGu'
const myData = 'Hello,rEaCt'
//创建虚拟DOM
const VDOM = (
<h2 className="title" id={myId.toLowerCase()}>
<span style={{color:'white', fontSize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
)
9-1-5 只能有一个根标签
如果我们复制一下上面的 <h2>
,会报错,这是因为只能有一个根标签,所以添加一个 <div>
将两个 <h2>
包起来。(如果不想出现讨厌的div可以使用Fragment标签作为占位标签,在实际的HTML中不会出现)
const myId = 'aTgUiGu'
const myData = 'Hello,rEaCt'
//创建虚拟DOM
const VDOM = (
<div>
<h2 className="title" id={myId.toLowerCase()}>
<span style={{color:'white', fontSize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
<h2 className="title" id={myId.toUpperCase()}>
<span style={{color:'white', fontSize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
</div>
)
9-1-6 标签必须闭合
如果我们添加一个文本输入框,如果写成 <input type="text">
会报错,这是因为,JSX 里的标签必须闭合,所以,应该写成 <input type="text"/>
。
const myId = 'aTgUiGu'
const myData = 'Hello,rEaCt'
//创建虚拟DOM
const VDOM = (
<div>
<h2 className="title" id={myId.toUpperCase()}>
<span style={{color:'white', fontSize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
<input type="text"/>
</div>
)
9-1-7 标签首字母
- 若小写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素,则报错。
- 若大写字母开头,react将去渲染对应的组件,若组件没有定义,则报错。
9-1-8 JSX中的html解析问题
使用 dangerouslySetInnerHTML 属性可以将一个字符串解析成 HTML,并渲染在 React 组件中。它的使用方法是在组件标签上添加 dangerouslySetInnerHTML
属性,值为一个包含 \_html
属性的对象,如下所示:
const RenderHtml = () => {
const htmlString = '<h1>Hello World</h1>';
return <div dangerouslySetInnerHTML={{ __html: htmlString }}></div>;
};
在上面的例子中,我们通过将包含 \_html
属性的对象传递给 dangerouslySetInnerHTML
属性,将 htmlString 中的 <h1>
元素渲染到了页面上。需要注意的是,由于使用 dangerouslySetInnerHTML 可能会导致跨站脚本攻击(XSS),因此应该谨慎使用。
另外,React 鼓励使用组件化
的方式来构建 UI,而不是直接在组件中使用 dangerouslySetInnerHTML
。如果有类似的需求,可以将字符串转成组件并在组件内部进行渲染。
9-2 条件渲染
9-1-1 if语句
比较简单的一种实现条件渲染·的方式,但是但是会降低代码的可读性和可维护性。
interface IComponentProps{
isShow:number
}
const MyComponent = (props:IComponentProps)=>{
const { isShow } = props;
if (isShow===1) {
return <div>Component content 111</div>
}else if(isShow===2){
return <div>Component content 222</div>
}
return null;
}
export default MyComponent
9-1-2 三元运算符
当我们想提前退出或者什么都不显示时,if 语句会很有用。但是,如果我们不想写一个与返回的 JSX 分开的条件,而是直接在其中写呢?那就可以使用三元表达式来编写条件。
在 React 中,我们必须在 JSX 中包含表达式,而不是语句。这就是为什么我们在 JSX 中只能使用三元表达式,而不是 if 语句来编写条件。
例如,在移动设备的屏幕上显示一个组件,而在更大的屏幕上显示另一个组件,就可以使用三元表达式来实现:
export default function App() {
const isMobile = useWindowSize()
return (
<main>
<Header />
{isMobile ? <MobileChat /> : <Chat />}
</main>
)
}
其实,不必将这些三元表达式包含在 JSX 中,可以将其结果分配给一个变量,然后在需要的地方使用即可:
export default function App() {
const isMobile = useWindowSize();
const ChatComponent = isMobile ? <MobileChat /> : <Chat />;
return (
<main>
<Header />
<Sidebar />
{ChatComponent}
</main>
)
}
9-1-3 &&运算符
在许多情况下,我们可能想要使用三元表达式,但是如果不满足条件,就不显示任何内容。那代码会是这样的:
condition ? <Component /> : null
可以使用 && 运算符来简化:
const MyComponent = ()=>{
let hasFinished:boolean = true
return <Fragment>
{hasFinished&&(<p>已经到底啦!</p>)}
</Fragment>
}
export default MyComponent
如果条件为真,则逻辑 && 运算符之后的表达式将是输出。如果条件为假,React 会忽略并跳过表达式。
9-1-4 switch
过多的 if 语句会导致组件变得混乱,可以将多个条件提取到包含 switch 语句的单独的组件中(根据组件逻辑的复杂程度来选择是否提取到单独的组件)。下面来看一个简单的菜单切换组件:
interface IComponentProps{
menu:number
}
const MenuItem:React.FC<IComponentProps> = ({menu})=>{
switch(menu){
case 1:
return <h1>menu1</h1>;
case 2:
return <h1>menu2</h1>;
case 3:
return <h1>menu3</h1>;
default:
return null;
}
}
const Menu = ()=>{
const [menu, setMenu] = useState(1);
const toggleMenu = () => {
setMenu((m) => {
if (m === 3) return 1;
return m + 1;
});
}
return (
<>
<MenuItem menu={menu} />
<button onClick={toggleMenu}>切换菜单</button>
</>
);
}
export default Menu
由于使用带有 switch 语句的 MenuItem 组件父菜单组件不会被条件逻辑弄乱,可以很容易地看到给定 menu 状态将显示哪个组件。需要注意,必须为 switch case 运算符使用默认值,因为在 React 中,组件始终需要返回一个元素或 null。
9-1-5 枚举
在 TS 中,当对象用作键值对的映射时,它可以用作枚举:
// 定义类型
interface IMenuProps{
state:number
}
type EnumStates = {
[key: number]: JSX.Element;
} & {
default: JSX.Element;
};
const Menu1 = ()=>{
return <h1>menu1</h1>;
}
const Menu2 = ()=>{
return <h1>menu2</h1>;
}
const Menu3 = ()=>{
return <h1>menu3</h1>;
}
// 创建可用作枚举的对象
const ENUM_STATES:EnumStates = {
1: <Menu1 />,
2: <Menu2 />,
3: <Menu3 />,
default: <div>无匹配的状态</div>
};
const Menu:React.FC<IMenuProps> = ({state})=>{
// 渲染这个枚举对象的函数
return (
<>
{ENUM_STATES[state]||ENUM_STATES.default}
</>
);
}
export default Menu
9-1-6 JSX库
JSX Control Statements 库扩展了 JSX 的功能,从而可以直接使用 JSX 实现条件渲染。它是一个 Babel 插件,可以在转译过程中将类似组件的控制语句转换为对应的 JavaScript。
安装babel-plugin-jsx-control-statements
包并修改 Babel 配置后,可以像这样重写应用程序:
export default function App(props) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
//...
return (
<Choose>
<When condition={isLoggedIn}>
<button>Logout</button>;
</When>
<When condition={!isLoggedIn}>
<button>Login</button>;
</When>
</Choose>
);
}
当然,不建议这样来编写条件语句,这样会导致代码的可读性变差,并且 JSX 允许使用强大的 JavaScript 功能来自己处理条件渲染,无需添加模板组件即可启用它。
9-1-7 高阶组件
React中的高阶组件(HOC)可以用来实现条件渲染,但这并不是HOC的主要作用。HOC是一种函数,它接收一个组件作为参数,并返回一个新的增强过的组件。通过在HOC中对传入的组件进行包装和操作,我们可以在不改变原始组件代码的情况下给它添加一些行为或者功能。
当我们需要根据条件来渲染一个组件时,可以通过定义一个HOC来实现。比如,我们可以定义一个名为withConditionalRendering的HOC,该HOC根据某个条件判断是否渲染组件。该HOC的代码可能如下所示:
function withConditionalRendering(Component) {
return function WrappedComponent(props) {
const { condition } = props;
if (condition) {
return <Component {...props} />;
} else {
return null;
}
}
}
在上述代码中,withConditionalRendering接收一个组件作为参数,并返回一个新的包装过的组件WrappedComponent,该组件根据传入的condition属性来判断是否渲染原始的组件。若condition为true,则渲染原始组件;否则,返回null。
使用该HOC包装一个组件,例如一个按钮组件,可以这样写:
const ConditionalButton = withConditionalRendering(Button);
然后,我们可以将conditional属性传递给ConditionalButton组件以控制组件的渲染:(相当于把参数传入到函数里面,有一个动态的过程)
<ConditionalButton condition={someCondition} />
通过这种方法,我们可以在React中实现条件渲染。当然,React还提供了其他许多适用于不同场景的渲染技术,如条件渲染、列表渲染、动态渲染等等,开发者可以根据需求进行选择。
9-1-8 注意事项
小心0
来看一个常见的渲染示例,当数组中存在元素时才渲染内容:
{gallery.length && <Gallery slides={gallery}>}
预想的结果是,数组存在元素时渲染内容,不存在元素时什么都不渲染。但是,页面上得到了 “0”。这是因为在使用与运算符时,一个假的左侧值(如 0)会立即返回。在JavaScript中,布尔运算法不会将其结果转化为布尔值。所以,React 将得到的值放入DOM中,与 false 不同的是,0 是一个有效的 React 节点,所以最终会渲染成0。
那该如何避免这个问题呢?可以显式的将条件转换为布尔值,当表达式结果为false时,就不会在页面中渲染了:
gallery.length > 0 && jsx
!!gallery.length && jsx
Boolean(gallery.length) && jsx
或者使用三元表达式来实现:
{gallery.length ? <Gallery slides={gallery} /> : null}
优先级
与运算符(&&)比或运算符(||)具有更高的优先级。所以,要格外小心使用包含与运算符的 JSX 条件:
user.anonymous || user.restricted && <div className="error" />
这样写就相当于:
user.anonymous || (user.restricted && <div className="error" />)
这样,与运算符左侧为真时就会直接返回,而不会继续执行后面的代码。所以,多数情况下,看到或运算符时,就将其使用括号括起来,避免因为优先级问题而渲染出错:
{(user.anonymous || user.restricted) && <div className="error" />}
嵌套三元表达式
三元表达式适合在两个JSX之间进行切换,一旦超过两个项目,代码就会变得糟糕:
{
isEmoji
? <EmojiButton />
: isCoupon
? <CouponButton />
: isLoaded && <ShareButton />
}
有时使用 && 来实现会更好,不过一些条件判断会重复:
{isEmoji && <EmojiButton />}
{isCoupon && <CouponButton />}
{!isEmoji && !isCoupon && isLoaded && <ShareButton />}
当然,这种情况下,使用 if 语句可能是更好的选择:
const getButton = () => {
if (isEmoji) return <EmojiButton />;
if (isCoupon) return <CouponButton />;
return isLoaded ? <ShareButton /> : null;
};
避免JSX作为条件
通过 props 传递的 React 元素能不能作为判断条件呢?来看一个简单的例子:
const Wrap = (props) => {
if (!props.children) return null;
return <div>{props.children}</div>
};
我们希望 Wrap 在没有包含内容时呈现 null,但 React 不是这样工作的:
- props.children 可以是一个空数组,例如
<Wrap>{\[].map(e => <div />)}</Wrap>
- children.length 也失败了:children 也可以是单个元素,而不是数组,例如:
(<Wrap><div /></Wrap>)
- React.Children.count(props.children)支持单个子项和多个子项,但会认为
<Wrap>{false && 'hi'}{false && 'there'}</Wrap>
包含 2 个项,而实际上没有任何子项 - React.Children.toArray(props.children) 移除无效节点,例如 false。然而,对于一个空片段,仍然是正确的:
<Wrap><>\</><Wrap>
- 如果将条件渲染移动到组件内:
<Wrap><Div hide /></Wrap>
与Div = (p) => p.hide ?null : <div />
,在 Wrap 渲染时永远无法知道它是否为空,因为 react 只会在父级之后渲染子级 div,而有状态的子级可以独立于其父级重新渲染。
因此,不要将JSX作为判断条件,避免出现一些难以预料的问题。
重新挂载还是更新
用三元表达式编写的 JSX 感觉就像是完全独立的代码:
{hasItem ? <Item id={1} /> : <Item id={2} />}
当 hasItem 改变时,React 并不知道也不关心我们写了什么,它所看到的只是 Item 元素在同一个位置,所以它保持挂载的实例,更新 props。上面的代码等价于 <Item id={hasItem ? 1:2} />
。
注意:如果三元表达式包含的是不同的组件,如 {hasItem ? <Item1 /> : <Item2 />}
,hasItem改变时,React 会重新挂载,因为 Item1 无法更新为 Item2。
上述情况会导致一些意外的行为:
{
mode === 'name'
? <input placeholder="name" />
: <input placeholder="phone" />
}
这里,如果在 name 的 input 中输入了一些内容,然后切换模式(mode),在 name 中输入内容的就会泄漏到 phone 的 input 中,这可能会对依赖于先前状态的复杂更新机制造成更大的破坏。
这里的一种解决方法是使用 key。通常,我们用它来渲染列表,但它实际上是 React 的元素标识提示——具有相同 key 的元素是相同的逻辑元素:
{
mode === 'name'
? <input placeholder="name" key="name" />
: <input placeholder="phone" key="phone" />
}
另一种方法是用两个单独的 && 块来替换三元表达式。当 key 不存在时,React 会回退到子数组中项目的索引,因此将不同的元素放在不同的位置与显式定义 key 的效果是一样的:
{mode === 'name' && <input placeholder="name" />}
{mode !== 'name' && <input placeholder="phone" />}
9-3 列表渲染
在React中,我们通常使用数组来表示一组数据。当需要将这组数据渲染成列表时,可以使用列表渲染的方式。
具体方法是利用 Array.prototype.map()
方法,将数组中的每个元素映射成一个新的 React 元素,并构建成一个新的数组返回。在 JSX 中,我们可以使用花括号 {} 将这个数组包裹起来,使得 JSX 能够正确地将它渲染成一个列表。
const users = [
{ id: 1, name: 'Alice', age: 23 },
{ id: 2, name: 'Bob', age: 20 },
{ id: 3, name: 'Charlie', age: 25 },
];
const UserList = ()=>{
return (
<ul>
{users.map(user => (
<li key={user.id}>
<div>Name: {user.name}</div>
<div>Age: {user.age}</div>
{user.age < 21 ? (
<div>You must be 21 or older to access this site.</div>
) : (
<button onClick={() => alert(`Hello, ${user.name}!`)}>
Click me!
</button>
)}
</li>
))}
</ul>
);
}
export default UserList
9-4 事件处理
React的事件处理是通过在组件中定义事件处理函数来实现的。
在React中,我们可以通过使用JSX来定义组件,并在其内部使用JavaScript语法。当需要对某个元素绑定一个事件时,我们可以在该元素上添加一个事件监听器,通常是通过on*属性的形式,比如onClick、onMouseOver等等。
当事件被触发时,React会自动调用相应的事件处理函数,并将事件对象作为参数传递给该函数。我们可以在事件处理函数中使用event对象来获取事件的相关信息,比如鼠标点击的位置、键盘按键等等。
如表单输入事件:
class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({ value: event.target.value });
}
render() {
return (
<form>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<p>You entered: {this.state.value}</p>
</form>
);
}
}
另外,在React中还有一种特殊的事件处理方式,即使用“受控组件”。通过将React组件的状态与用户输入的值进行绑定,我们可以实现实时更新组件的显示,这种方式在表单处理中尤为常见。具体实现方式是在组件中定义一个处理输入改变的事件处理函数,将输入值与组件状态进行绑定,再通过setState()方法更新组件状态,从而实现实时更新显示。
总之,React的事件处理提供了灵活且简单的方式,使得我们可以轻松地实现各种事件响应和交互效果。
10. Redux
10-1 Redux的引入
Redux 帮你管理“全局”状态 - 应用程序中的很多组件都需要的状态。
Redux 的三大核心是:store、action 和 reducer。
- Store: Redux 中的 store 是一个状态容器,用于管理应用程序的状态,同时提供了访问状态和更新状态的方法。
- Action: Redux 中的 action 是一个普通的 JavaScript 对象,用于描述发生了什么事情。它包含了一个 type 字段,用于表示这个 action 的类型,并可以包含任意其他字段用于描述这个 action 的数据。
- Reducer: 在 Redux 中,reducer 是一个纯函数,用于接收当前的 state 和一个 action,然后根据 action 的类型返回一个新的 state。这个新的 state 会被替换原来的 state,从而更新整个应用程序的状态。函数签名是:(state, action) => newState。
举例说明:
当一个用户在应用程序中点击了某个按钮,我们可以将这个按钮点击事件表示成一个 action,例如:
const buttonClicked = {
type: 'BUTTON_CLICKED',
payload: {
buttonText: 'Click Me'
}
};
这里的 type 就表示这个操作的类型是 BUTTON_CLICKED,payload 用于描述这个操作附带的数据。
接着,我们可以将这个 action 发送给 store,store 将会调用相应的 reducer 来生成一个新的 state。下面是一个 reducer 的例子,它接受先前定义的 action 类型并根据该类别更新状态
function buttonReducer(state = { pressed: false }, action) {
switch(action.type) {
case 'BUTTON_CLICKED':
return Object.assign({}, state, { pressed: true });
default:
return state;
}
}
当这个 reducer 收到来自 store 的 buttonClicked action 时,它会返回一个新的 state 对象,其中 pressed 属性被设置为 true。
最后,store 将使用新的 state 对象更新整个应用程序的界面,实现页面上按钮的状态变化。
当 dispatch 方法被调用时,它会调用 reducer 来生成一个新的 state,并触发所有的订阅者更新应用程序的界面。
需要注意的是,Redux 应用程序中的每个改变都必须通过 dispatch 一个 action 来进行,所以 dispatch 方法是 Redux 最重要的 API 之一。
store.dispatch(buttonClicked);
当 dispatch 方法被调用时,它会调用 reducer 来生成一个新的 state,并触发所有的订阅者更新应用程序的界面。
需要注意的是,Redux 应用程序中的每个改变都必须通过 dispatch 一个 action 来进行,所以 dispatch 方法是 Redux 最重要的 API 之一。虽然在某些简单的应用场景中可能不需要显式地使用 dispatch,但是在实际的开发中,它是 Redux 最重要的核心概念之一,可以帮助我们实现应用状态的管理和复杂功能的实现。
不一定非要用,很多项目 context 就已经够用了。在以下情况下使用 Redux:
- 应用中有很多 state 在多个组件中需要使用
- 应用 state 会随着时间的推移而频繁更新
- 更新 state 的逻辑很复杂
- 中型和大型代码量的应用,很多人协同开发
10-2 Redux的使用例子
10-2-1 借助Redux实现一个简单的计数器
参考博客:【Redux 最佳实践 Redux Toolkit】
1. 在终端中切换到您的项目目录下,运行以下命令安装Redux和 @reduxjs/toolkit :
pnpm install redux
pnpm install @reduxjs/toolkit
(使用 @reduxjs/toolkit 时,通常不需要再单独安装 Redux 包,因为 @reduxjs/toolkit 可以作为 Redux 的一个补充库使用,包含了 Redux 常用的 API 和工具函数,能够大大简化 Redux 的使用和配置,同时带来更好的性能和开发体验。在 @reduxjs/toolkit 中,包含了 Redux 使用的核心库以及许多常用的 middleware(例如 Redux Thunk、Redux-Logger 等),可以轻松地进行封装和配置,使得 Redux 的使用更加方便和易用。但是,如果在某些场景下需要仍需要直接使用 Redux 的 API 或引入其他第三方 Redux 相关的包时,可能需要单独安装 redux 包。)
2. 安装完毕后,您需要安装React-Redux库,用于React组件和Redux store之间的连接:
pnpm install react-redux
3. 在项目里面新增store文件夹,在里面新增一些文件:
其中:
- src/store/index.ts:Redux store的初始化和配置。
- src/store/reducers.ts:Redux reducer函数的存放位置。
- src/store/actions.ts:Redux action creator函数的存放位置。
4. 在 src/store/index.ts 中初始化 Redux store 并引入 reducer 函数:
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
const store = configureStore({
reducer: rootReducer,
});
export default store;
5. 在 src/store/reducers.ts 中定义 reducer:
// 定义一个 Action 类型,表示对状态的操作
interface Action {
type: string; // 操作类型
payload?: any; // 操作参数(可选)
}
// 定义一个 State 类型,表示状态对象的数据结构
interface State {
count: number; // 计数器的值
}
// 初始化状态对象
const initialState: State = {
count: 0, // 计数器初始值为 0
};
// 定义 reducer 函数,用于更新状态对象
function reducer(state = initialState, action: Action) {
switch (action.type) {
case 'INCREMENT': {
// 如果动作类型为 INCREMENT,则将计数器加一
return {
...state, // 复制原有状态对象
count: state.count + 1, // 更新计数器的值
};
}
case 'DECREMENT': {
// 如果动作类型为 DECREMENT,则将计数器减一
return {
...state, // 复制原有状态对象
count: state.count - 1, // 更新计数器的值
};
}
default:
return state; // 如果动作类型不匹配,则返回原有状态对象
}
}
export default reducer; // 导出 reducer 函数
6. 在 src/store/actions.ts 中定义 action creator:
export const increment = () => ({
type: 'INCREMENT',
});
export const decrement = () => ({
type: 'DECREMENT',
});
7. 在 React 组件中使用 connect 连接 store 和组件,并获取需要的数据和 dispatch 函数:
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './store/actions'; // 引入操作 store 的 action 函数
// 组件接收的 props 类型
interface Props {
count: number;
increment: () => void;
decrement: () => void;
}
// 状态类型
interface State{
count:number
}
function Counter(props: Props) {
const { count, increment, decrement } = props; // 从 props 中获取 count、increment 和 decrement
return (
<div>
<p>Current count: {count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
// 将 store 中的状态数据映射到组件的 props 中
function mapStateToProps(state: State) {
return {
count: state.count, // 返回一个对象,count 对应的是 state 中的 count 属性
};
}
// 将 store 中的操作状态数据的方法映射到组件的 props 中
const mapDispatchToProps = {
increment, // 增加计数器的方法
decrement, // 减少计数器的方法
};
// 使用 connect 函数将 mapStateToProps 和 mapDispatchToProps 的结果与 Counter 组件连接起来
// 这样就能够在组件中访问到 store 中的数据和方法了
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
8. 在父组件(例如 main.tsx)中使用 Provider 组件将 store 传入 React 组件树中:
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './App'
import { Provider } from 'react-redux';
import store from './store';
import Counter from "./practice_redux";
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
{/* <App /> */}
<Provider store={store}>
<Counter />
</Provider>
</React.StrictMode>,
)
10-2-2 传递和接收参数
对于上面的计时器例子,如果我们想自定义每次加减的值,则需要在【actions.ts】中增加可选参数payload:
export const increment = (amount:number) => ({
type: 'INCREMENT',
payload:amount
});
export const decrement = (amount:number) => ({
type: 'DECREMENT',
payload:amount
});
接着在【reducers.ts】里面接收到这个可选参数:
// 定义一个 Action 类型,表示对状态的操作
interface Action {
type: string; // 操作类型
payload?: number; // 操作参数(可选)
}
// 定义一个 State 类型,表示状态对象的数据结构
interface State {
count: number; // 计数器的值
}
// 初始化状态对象
const initialState: State = {
count: 0, // 计数器初始值为 0
};
// 定义 reducer 函数,用于更新状态对象
function reducer(state = initialState, action: Action) {
switch (action.type) {
case 'INCREMENT': {
const incrementValue = action.payload || 1; // 如果没有自定义加的值,则默认为 1
// 如果动作类型为 INCREMENT,则将计数器加一
return {
...state,
count: state.count + incrementValue,
};
}
case 'DECREMENT': {
const decrementValue = action.payload || 1; // 如果没有自定义减的值,则默认为 1
// 如果动作类型为 DECREMENT,则将计数器减一
return {
...state, // 复制原有状态对象
count: state.count - decrementValue, // 更新计数器的值
};
}
default:
return state; // 如果动作类型不匹配,则返回原有状态对象
}
}
export default reducer; // 导出 reducer 函数
最后在组件里面传入参数:
import { connect } from 'react-redux';
import { increment, decrement } from './store/actions'; // 引入操作 store 的 action 函数
import { useState } from 'react';
// 组件接收的 props 类型
interface Props {
count: number;
increment: (amount:number) => void; //传入参数
decrement: (amount:number) => void; //传入参数
}
// 状态类型
interface State{
count:number
}
function Counter(props: Props) {
const { count, increment, decrement } = props; // 从 props 中获取 count、increment 和 decrement
// 设置变量
const [amount, setAmount] = useState(1);
return (
<div>
<input value={amount} onChange={(e) => setAmount(+e.target.value)}/>
<p>Current count: {count}</p>
<button onClick={()=>decrement(amount)}>-</button>
<button onClick={()=>increment(amount)}>+</button>
</div>
);
}
// 将 store 中的状态数据映射到组件的 props 中
function mapStateToProps(state: State) {
return {
count: state.count, // 返回一个对象,count 对应的是 state 中的 count 属性
};
}
// 将 store 中的操作状态数据的方法映射到组件的 props 中
const mapDispatchToProps = {
increment, // 增加计数器的方法
decrement, // 减少计数器的方法
};
// 使用 connect 函数将 mapStateToProps 和 mapDispatchToProps 的结果与 Counter 组件连接起来
// 这样就能够在组件中访问到 store 中的数据和方法了
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
10-2-3 关于Redux中使用异步请求的处理
Redux本身是一个状态管理库,它并不关心具体实现异步请求的方式,因此React中确实可以使用其他方法如axios等库来处理异步请求。
但是,由于Redux的数据流架构是单向的、纯函数式的,只有通过dispatch action才能触发state的更新,因此在redux中实现异步请求通常需要使用中间件来处理异步操作。其中,使用最多的中间件是redux-thunk和redux-saga。在RTK中,使用createAsyncThunk方法来实现这一功能。
1、新增一个action:(store的actions.ts里面)
import {User} from './reducers/userSlice';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const increment = (amount:number) => ({
type: 'INCREMENT',
payload:amount
});
export const decrement = (amount:number) => ({
type: 'DECREMENT',
payload:amount
});
// 异步请求获取用户列表
// 注:createAsyncThunk 方法接收一个 action type 和一个包含异步逻辑的回调函数,返回一个可用于派发的 action creator 函数。
export const fetchUsers = createAsyncThunk<User[]>('user/fetchUsers', async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
return res.json();
});
2、新增一个reducer,命名为userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { fetchUsers } from '../actions';
interface User {
id: number;
name: string;
}
interface UserState {
data: User[];
isLoading: boolean;
error: string | null;
}
const initialState: UserState = {
data: [],
isLoading: false,
error: null
};
// 注:createSlice 方法接收一个对象形式的参数,包含 name、initialState、reducers 和 extraReducers 四个属性。
// extraReducers 是用于处理异步请求 action 的 reducer 部分,使用 builder 对象来接收处理函数,
// 通过 addCase 方法可以根据请求状态(pending、fulfilled 或 rejected)来处理 state 的更新。
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchUsers.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action: PayloadAction<User[]>) => {
state.isLoading = false;
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
}
});
export type {User,UserState};
export {userSlice};
3、合并多个slice、导出状态类型:(store的index.ts里面)
import { configureStore } from '@reduxjs/toolkit';
import {CounterState,counterReducer} from './reducers/counterSlice';
import {UserState,userSlice} from './reducers/userSlice';
const store = configureStore({
// 合并多个Slice
reducer: {
counter:counterReducer,
user:userSlice.reducer
}
});
// 导出状态类型
export type RootState = {
user: UserState;
counter:CounterState
};
export default store;
4、在组件里使用:
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from './store/actions'; // 引入操作 store 的 action 函数
import { RootState } from './store';
import { AnyAction } from '@reduxjs/toolkit';
// dispatch 方法需要传入 action 对象作为参数,而 createAsyncThunk 返回的是一个 AsyncThunkAction。
// 为了解决这个问题,我们可以使用 type guard 或者 as 语法来将 AsyncThunkAction 转换成 AnyAction 类型,以便它可以被 dispatch 函数正常使用。
export const UserList = () => {
const dispatch = useDispatch();
const users = useSelector((state: RootState) => state.user.data);
const isLoading = useSelector((state: RootState) => state.user.isLoading);
const error = useSelector((state: RootState) => state.user.error);
useEffect(() => {
// 使用类型断言(as)解决上述问题
dispatch(fetchUsers() as unknown as AnyAction);
}, [dispatch]);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>{error}</div>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
注:现在项目目录变成了:
11. React Router
11-1 路由的概念
1. 什么是路由?
一个路由就是一个映射关系(key:value)。 key为路由路径,value可能是function/component
2. 路由分类:
后台路由
node服务器端路由,value是function,用来处理客户端提交的请求并返回一个响应数据。 当node接收到一个请求时, 根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据:
router.get(path, function(req, res))
前台路由
浏览器端路由,value是component,当请求的是路由path时,浏览器端前没有发送http请求,但界面会更新显示对应的组件。当浏览器的hash变为#about时, 当前路由组件就会变为About组件:
<Route path="/about" component={About}>
vue 把 VueRouter作为一个对象,而React 把router作为一个组件。
3. 关于url中的#
1. 理解#
① '#'代表网页中的一个位置。其右面的字符,就是该位置的标识符
② 改变#不触发网页重载
③ 改变#会改变浏览器的访问历史
2. 操作#
① window.location.hash读取#值
② window.onhashchange = func 监听hash改变
11-2 React Router的使用
- 明确好界面中的导航区、展示区
- 导航区的a标签改为Link标签
- 展示区写Route标签进行路径的匹配
- 的最外侧包裹了一个或
pnpm install react-router-dom @types/react-router-dom
// 1. About.tsx
import {Link} from 'react-router-dom'
function About() {
return <>
<div>About组件内容</div>
<button><Link to="/">回退</Link></button>
</>
}
export default About
// 2. Repos.tsx
import {Component} from 'react'
import {Link} from 'react-router-dom'
export default class Repos extends Component {
render() {
return (<>
<div>Repos组件</div>
<button><Link to="/">回退</Link></button>
</>
)
}
}
// 3. Home.tsx
import {Component} from 'react'
import {Link} from 'react-router-dom'
export default class Home extends Component {
render() {
return (
<div>
<h2>Hello, React Router!</h2>
<ul>
<li><Link to="/about">About组件</Link></li>
<li><Link to="/repos">Repos组件</Link></li>
</ul>
</div>
)
}
}
export default Home
import React from 'react';
import { BrowserRouter as Router,Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Repos from './pages/Repos';
const DemoRouter: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/repos" element={<Repos />} />
</Routes>
</Router>
);
};
export default DemoRouter;
11-3 路由组件与一般组件
-
写法不同:
- 一般组件:
<Demo/>
- 路由组件:
<Route path="/demo" component={Demo}/>
- 一般组件:
-
存放位置不同:
- 一般组件:
components
- 路由组件:
pages
- 一般组件:
-
接收到的props不同
-
一般组件:写组件标签时传递了什么,就能收到什么
-
路由组件:接收到三个固定的属性:
history: go: ƒ go(n) goBack: ƒ goBack() goForward: ƒ goForward() push: ƒ push(path, state) replace: ƒ replace(path, state) location: pathname: "/about" search: "" state: undefined match: params: {} path: "/about" url: "/about"
-
11-4 编程式路由导航
借助this.props.history对象上的API对操作路由跳转、前进、后退
-this.props.history.push()
-this.props.history.replace()
-this.props.history.goBack()
-this.props.history.goForward()
-this.props.history.go()
以使用 navigate() 方法进行编程式导航为例。它可以在代码中调用,以在路由切换时进行重定向:
import { navigate } from 'react-router-dom';
function LoginPage() {
const handleLogin = () => {
// 模拟登录过程
const token = '123';
// 登录成功后进行路由跳转
if (token) {
navigate('/home');
}
};
return (
<div>
{/* 登录表单 */}
<button onClick={handleLogin}>登录</button>
</div>
);
}
11-5 路由相关API
Route
路由组件,注册路由
<Route path='/xxxx' component={Demo}/> //属性1: path="/xxx" 属性2: component={xxx}
- 根路由组件: path="/"的组件, 一般为App
- 子路由组件: 子配置的组件
IndexRoute
默认子路由组件
hashHistory
用于Router组件的history属性,路由的切换由URL的hash变化决定,即URL的#部分发生变化
作用: 为地址url生成?_k=hash, 用于内部保存对应的state
Link
<Link to="/xxxxx">Demo</Link>
路由链接组件。 属性1: to="/xxx" 属性2: activeClassName="active"
Routes
-
通常情况下,path和element是一一对应的关系。
-
Routes可以提高路由匹配效率(单一匹配)。
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/repos" element={<Repos />} />
</Routes>
NavLink与封装NavLink
NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
路由的严格匹配与模糊匹配
-
默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
-
开启严格匹配
<Route exact={true} path="/about" component={About}/>
- 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
嵌套路由
-
注册子路由时要写上父路由的path值
-
路由的匹配是按照注册路由的顺序进行的
11-6 路由传递参数
- params参数
在注册路由里声明路由格式:
import React from 'react';
import { BrowserRouter as Router,Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Repos from './pages/Repos';
const DemoRouter: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about/:name?/:age?" element={<About />} />
<Route path="/repos" element={<Repos />} />
</Routes>
</Router>
);
};
export default DemoRouter;
跳转时传递相关参数:
import {Component} from 'react'
import {Link} from 'react-router-dom'
export default class Home extends Component {
render() {
return (
<div>
<h2>Hello, React Router!</h2>
<ul>
<li><Link to="/about/tom/18">About组件</Link></li>
<li><Link to="/repos">Repos组件</Link></li>
</ul>
</div>
)
}
}
使用 useParams 获取 url 中的参数:
import {Link,useParams} from 'react-router-dom'
function About() {
const { name, age } = useParams<{ name: string; age: string }>(); // 使用 useParams 获取 url 中的参数
return <>
<div>About组件内容{name}{age}</div>
<button><Link to="/">回退</Link></button>
</>
}
export default About
2. search参数
路跳转时传递相关参数:(无需注册)
import {Component} from 'react'
import {Link} from 'react-router-dom'
export default class Home extends Component {
render() {
return (
<div>
<h2>Hello, React Router!</h2>
<ul>
<li><Link to="/about/tom/18">About组件</Link></li>
<li><Link to="/repos?name=tom&age=18">Repos组件</Link></li>
</ul>
</div>
)
}
}
使用 useLocation 获取 url 中的参数::
import {Link} from 'react-router-dom'
import { useLocation } from 'react-router-dom';
function Repos() {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const name = searchParams.get('name');
const age = searchParams.get('age');
return (
<>
<div>Repos组件{name}{age}</div>
<button><Link to="/">回退</Link></button>
</>
)
}
export default Repos
备注:获取到的search是urlencoded编码字符串,需要借助相关函数或方法解析
11-7 BrowserRouter与HashRouter的区别
在 React Router v6 中,BrowserRouter 和 HashRouter 组件依然存在,并且其功能与之前版本的相同。主要区别在于路由实现的方式不同,它们分别使用了 HTML5 History API 和 URL 哈希值来进行路由控制。
具体来说,BrowserRouter 使用 HTML5 History API 来控制应用程序的路由,这意味着路由器将直接操作浏览器的历史记录,使得 URL 发生变化,并允许用户使用浏览器导航按钮进行导航。例如:
import { BrowserRouter, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Route … />
</BrowserRouter>
);
}
而 HashRouter 则是使用 URL 哈希值来进行路由控制。这种方法不会直接更改浏览器的历史记录,而是通过在 URL 的末尾添加一个 hash 值来模拟路由机制。例如:
import { HashRouter, Route } from 'react-router-dom';
function App() {
return (
<HashRouter>
<Route … />
</HashRouter>
);
}
需要注意的是,对于大多数应用程序来说,BrowserRouter 是更好的选择,因为它更符合历史记录和用户体验的标准。但是在一些特殊情况下,比如静态网站或在无服务器环境下部署的应用程序中,HashRouter 可能更为适合。
11-8 解决多级路径刷新页面样式丢失的问题
在 React Router 中,当我们进行多级路径的路由跳转时,页面可能会出现样式丢失的问题,这是因为浏览器在刷新页面时会重新加载所有资源文件,而原来在不同目录下的 CSS 和 JavaScript 文件的路径也会发生变化,导致浏览器无法正确加载这些文件。
为了解决这个问题,我们可以使用相对路径或绝对路径来引用静态资源。具体来说,可以在文件名前加上 . 或 / 来表示当前目录或根目录,这样引用的路径就不会受到路由影响
例如,假设我们有一个 styles.css 文件,它在 src/styles/ 目录下,并且我们的路由配置如下:
<Router>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Router>
如果我们在 Home 组件中需要引用 styles.css,可以按照以下方式引用:
<link rel="stylesheet" href="../styles/styles.css" />
而在 About 组件中,则可以使用相对路径:
<link rel="stylesheet" href="./styles.css" />
这样,在进行路由跳转时,页面刷新后依然可以正确加载静态资源。
除了使用相对路径和绝对路径,还可以考虑使用 CDN 或将静态资源打包成 JavaScript 模块等方式来优化应用程序的性能和稳定性。
转载自:https://juejin.cn/post/7237426124669435961