实战
React表单组件
入门
重点在于change的时候改变state的值,类似vue的双向数据绑定v-model,即数据更新的时候页面同步更新,页面数据更新时数据源也能获得最新的值,只是Vue中设置在data中的属性默认绑定,React中需要state触发页面更新
使用原先较多测试组件的项目写基础
input组件
TypeScript
import React, { useState, ChangeEvent } from "react";
// 上下两种引入方式都可以
// import type { ChangeEvent } from "react";
function App() {
const [text, setText] = useState<string>("hello");
function handleChange(event: ChangeEvent<HTMLInputElement>) {
// event.target.value就是当前input的值
setText(event.target.value);
}
return (
<div>
<input defaultValue={text} onChange={handleChange} />
<button onClick={() => console.log(text)}>打印</button>
</div>
);
}
export default App;
- 受控组件:值同步到state,使用value属性,可获可控
- 非受控组件:值不同步到state,使用defaultValue属性,能够设置默认值,但是无法获得更改后最新的数值
- React推荐使用受控组件,看似繁琐(没有规律),但更加可控(有规律,可读)
textarea组件
TypeScript
function App() {
const [text, setText] = useState<string>("hello");
function handleChange(event: ChangeEvent<HTMLTextAreaElement>) {
// event.target.value就是当前input的值
setText(event.target.value);
}
function genHtml() {
return { __html: text.replaceAll("\n", "<br>") };
}
return (
<div>
{/* <input defaultValue={text} onChange={handleChange} /> */}
<textarea value={text} onChange={handleChange}></textarea>
{/* {text.replaceAll("\n", "<br>")} */}
{/* 上面这个方法中为了防止XSS注入,React会将br换成明文展示在页面而不是换行,可以通过下列方式解决 */}
<p dangerouslySetInnerHTML={genHtml()}></p>
</div>
);
}
export default App;
radio单选框
TypeScript
function App() {
const [gender, setGender] = useState("male");
function handleChange(event: ChangeEvent<HTMLInputElement>) {
// event.target.value就是当前input的值
setGender(event.target.value);
}
return (
<div>
<label htmlFor="radio1">男</label>
<input
type="radio"
id="radio1"
name="gender"
value="male"
checked={gender == "male"}
onChange={handleChange}
/>
<label htmlFor="radio2">女</label>
<input
type="radio"
id="radio2"
name="gender"
value="female"
checked={gender === "female"}
onChange={handleChange}
/>
<button onClick={() => console.log(gender)}>打印</button>
</div>
);
}
export default App;
checkbox复选
TypeScript
function App() {
const [selectedList, setSelectedList] = useState<string[]>([]);
function handleCityChange(event: ChangeEvent<HTMLInputElement>) {
// event.target.value就是当前input的值
const city = event.target.value;
if (selectedList.includes(city)) {
setSelectedList(
selectedList.filter((c) => {
if (c == city) return false;
return true;
}),
);
} else {
// 添加
setSelectedList(selectedList.concat(city));
}
}
return (
<>
{/* htmlFor 点击的时候也会触发切换 */}
<label htmlFor="checkbox1">北京</label>
<input
type="checkbox"
id="checkbox1"
value="beijing"
checked={selectedList.includes("beijing")}
onChange={handleCityChange}
/>
<label htmlFor="checkbox2">上海</label>
<input
type="checkbox"
id="checkbox2"
value="shanghai"
checked={selectedList.includes("shanghai")}
onChange={handleCityChange}
/>
<label htmlFor="checkbox3">深圳</label>
<input
type="checkbox"
id="checkbox3"
value="shenzhen"
checked={selectedList.includes("shenzhen")}
onChange={handleCityChange}
/>
{/* 方便表单获取和提交 */}
<input
type="hidden"
name="cityInput"
value={JSON.stringify(selectedList)}
/>
</>
);
}
export default App;
select下拉框
TypeScript
function App() {
const [lang, setLang] = useState("js");
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
// event.target.value就是当前input的值
setLang(event.target.value);
}
return (
<>
{/* 不设置state的话选择了没反应 */}
<select value={lang} onChange={handleChange}>
<option value="java">Java</option>
<option value="C++">C++</option>
<option value="python">Python</option>
</select>
</>
);
}
form表单组件
TypeScript
function App() {
function handleSubmit(event: ChangeEvent<HTMLFormElement>) {
event.preventDefault(); // 阻止默认行为,即不会提交到action
// 可以自行处理提交的逻辑
}
return (
<>
{/* 点击提交后会将相应的数据发送到action中填写的接口。提交的就是name和value */}
{/* 没有name,没有value的话会导致提交无法识别 */}
{/* 隐藏input(type:hidden)的价值就在于可以把想要提交的数据偷偷提交上去 */}
{/* onSubmit提交前调用的钩子函数,可以阻止默认行为,不然点击就直接提交了 */}
<form action="/api/post" onSubmit={handleSubmit}>
<input />
<br />
<textarea />
<input type="hidden" />
<button type="submit">提交</button>
</form>
</>
);
}
Ant Design 实现
为什么地址要添加参数,避免刷新的时候搜索数据丢失,不保存的话一刷新页面内容就重置了,保存了之后就算是刷新,由于地址附有参数,刷新时地址不会改变,所以仍能展示出搜索后的数据
同时也是为了避免组件之间的耦合,最好不要一搜索就更新列表组件或者一分页就更新列表组件,而是通过一个更加公共的比如地址栏进行搜索或者分页信息的传递,也能避免刷新后数据无法保存,比如搜索栏一刷新原来的搜索词就没有了,但是可以通过从路径获取参数来达到"保存"的效果
新建constants文件夹,其中设置index.tsx文件保存常用变量,跟router中导出常用变量类似
// 存储所有的常量
export const LIST_SEARCH_PARAM_KEY = "keyword";
搜索栏组件
ListSearch.tsx
TypeScript
import React, { FC, useEffect, useState } from "react";
import { Input } from "antd";
import type { ChangeEvent } from "react";
import { useNavigate, useLocation, useSearchParams } from "react-router-dom";
import { LIST_SEARCH_PARAM_KEY } from "../constants";
const { Search } = Input;
const ListSearch: FC = () => {
const [val, setVal] = useState("");
const nav = useNavigate();
const { pathname } = useLocation();
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setVal(event.target.value);
}
function handleSearch(value: string) {
// 跳转页面增加URL参数
nav({
pathname,
search: `${LIST_SEARCH_PARAM_KEY}=${value}`,
});
}
// 获取url参数,并设置到input value
const [searchParams] = useSearchParams();
useEffect(() => {
// 每当searchParams有变化就执行函数
// serchParams用来获得上面nav中设置的参数
const newVal = searchParams.get(LIST_SEARCH_PARAM_KEY) || "";
setVal(newVal);
}, [searchParams]);
return (
<Search
allowClear
placeholder="请输入关键字"
value={val}
onChange={handleChange}
onSearch={handleSearch}
style={{ width: "260px" }}
/>
);
};
export default ListSearch;
开发注册页
Register.module.scss
css
.contain{
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-image: linear-gradient(to top, #5ee7df 0%, #b490ca 100%);
// background-image: linear-gradient(to top, #9890e3 0%, #b1f4cf 100%);
}
Register.tsx
TypeScript
import React, { FC } from "react";
import { Typography, Space, Form, Input, Button } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import styled from "./Register.module.scss";
import { Link } from "react-router-dom";
import { LOGIN_PATHNAME } from "../router";
const { Title } = Typography;
const Register: FC = () => {
// any表示任意类型都可
const onFinish = (values: any) => {
console.log(values);
};
return (
<div className={styled.contain}>
<div>
<Space>
<Title level={2}>
<UserAddOutlined />
</Title>
<Title level={2}>注册新用户</Title>
</Space>
</div>
<div>
<Form
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
onFinish={onFinish}
>
<Form.Item label="用户名" name="userName">
<Input />
</Form.Item>
<Form.Item label="密码" name="passWord">
<Input.Password />
</Form.Item>
<Form.Item label="确认密码" name="conFirm">
<Input.Password />
</Form.Item>
<Form.Item label="昵称" name="nickName">
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Space>
{/* htmlType就是之前html里的type,只是前面设置属性被占用了,用这个一样的效果都是为了触发方法 */}
<Button type="primary" htmlType="submit">
注册
</Button>
</Space>
<Link to={LOGIN_PATHNAME}>已有账户,登录</Link>
</Form.Item>
</Form>
</div>
</div>
);
};
export default Register;
开发登录页
Login.module.scss
css
.contain{
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-image: linear-gradient(to top, #5ee7df 0%, #b490ca 100%);
// background-image: linear-gradient(to top, #9890e3 0%, #b1f4cf 100%);
}
Login.tsx
TypeScript
// 登陆页面
import React, { FC, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Space, Typography, Form, Input, Button, Checkbox } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import { REGISTER_PATHNAME } from "../router";
import styled from "./Login.module.scss";
const { Title } = Typography;
const USERNAME_KEY = "USERNAME";
const PASSWORD_KEY = "PASSWORD";
function rememberUser(username: string, password: string) {
localStorage.setItem(USERNAME_KEY, username);
localStorage.setItem(PASSWORD_KEY, password);
}
function deleteUser() {
localStorage.removeItem(USERNAME_KEY);
localStorage.removeItem(PASSWORD_KEY);
}
function getUser() {
return {
username: localStorage.getItem(USERNAME_KEY),
password: localStorage.getItem(PASSWORD_KEY),
};
}
const Login: FC = () => {
const nav = useNavigate();
// 第三方hook,即第三方提供的组件
const [form] = Form.useForm();
const onFinish = (values: any) => {
const { username, password, remember } = values;
if (remember) {
rememberUser(username, password);
} else {
deleteUser();
}
};
// 依赖不填写,默认在组件渲染完成后执行
useEffect(() => {
const { username, password } = getUser();
// 这里如果不小心写成了setFieldValue会报错,差了个s,这个只能输入一个参数,有s的才能输入多个
form.setFieldsValue({ username, password });
}, []);
return (
<div className={styled.contain}>
<div>
<Space>
<Title level={2}>
<UserAddOutlined />
</Title>
<Title level={2}>用户登录</Title>
</Space>
</div>
<div>
<Form
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
onFinish={onFinish}
// 默认remember设置为true
initialValues={{ remember: true }}
// 将返回值关联起来,即使其变成受控组件
form={form}
>
<Form.Item label="用户名" name="username">
<Input />
</Form.Item>
<Form.Item label="密码" name="password">
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
wrapperCol={{ offset: 6, span: 16 }}
>
{/* 表单需要有name和value才能提交,valuePropname就是将Checkbox的checked属性,即选中的属性(true || false)当作值 */}
<Checkbox>记住我</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 16 }}>
<Space>
<Button type="primary" htmlType="submit">
登录
</Button>
<Link to={REGISTER_PATHNAME}>注册新用户</Link>
</Space>
</Form.Item>
</Form>
</div>
</div>
);
};
export default Login;
表单校验
先在注册里进行校验,使用的都是antd里form的功能,然后复制到登录就行
Register.tsx
TypeScript
// 注册界面
import React, { FC } from "react";
import { Typography, Space, Form, Input, Button, message } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import styled from "./Register.module.scss";
import { Link } from "react-router-dom";
import { LOGIN_PATHNAME } from "../router";
const { Title } = Typography;
const Register: FC = () => {
// any表示任意类型都可
const onFinish = (values: any) => {
console.log(values);
};
return (
<div className={styled.contain}>
<div>
<Space>
<Title level={2}>
<UserAddOutlined />
</Title>
<Title level={2}>注册新用户</Title>
</Space>
</div>
<div>
<Form
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
onFinish={onFinish}
>
<Form.Item
label="用户名"
name="username"
rules={[
{ required: true, message: "请输入用户名" },
// string表示按长度计算区间范围,不然变成数字就成最小5最大20了
{
type: "string",
min: 5,
max: 20,
message: "字符长度在5-20之间",
},
{
pattern: /^\w+$/,
message: "只能是字母数字下划线",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
// required表示必填项,会出现红点
rules={[{ required: true, message: "请输入密码" }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="确认密码"
name="confirm"
// 依赖password属性,password变化会重新触发验证
dependencies={["password"]}
rules={[
{ required: true, message: "请确认输入的密码" },
// 这个校验传递进去的是一个函数
({ getFieldValue }) => ({
validator(_, value) {
if (getFieldValue("password") === value) {
return Promise.resolve();
} else {
return Promise.reject(new Error("两次密码不一致"));
}
},
}),
]}
>
<Input.Password />
</Form.Item>
<Form.Item label="昵称" name="nickname">
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Space>
{/* htmlType就是之前html里的type,只是前面设置属性被占用了,用这个一样的效果都是为了触发方法 */}
<Button type="primary" htmlType="submit">
注册
</Button>
<Link to={LOGIN_PATHNAME}>已有账户,登录</Link>
</Space>
</Form.Item>
</Form>
</div>
</div>
);
};
export default Register;
Login.tsx
TypeScript
// 登陆页面
import React, { FC, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Space, Typography, Form, Input, Button, Checkbox } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import { REGISTER_PATHNAME } from "../router";
import styled from "./Login.module.scss";
const { Title } = Typography;
const USERNAME_KEY = "USERNAME";
const PASSWORD_KEY = "PASSWORD";
function rememberUser(username: string, password: string) {
localStorage.setItem(USERNAME_KEY, username);
localStorage.setItem(PASSWORD_KEY, password);
}
function deleteUser() {
localStorage.removeItem(USERNAME_KEY);
localStorage.removeItem(PASSWORD_KEY);
}
function getUser() {
return {
username: localStorage.getItem(USERNAME_KEY),
password: localStorage.getItem(PASSWORD_KEY),
};
}
const Login: FC = () => {
const nav = useNavigate();
// 第三方hook,即第三方提供的组件
const [form] = Form.useForm();
const onFinish = (values: any) => {
const { username, password, remember } = values;
if (remember) {
rememberUser(username, password);
} else {
deleteUser();
}
};
// 依赖不填写,默认在组件渲染完成后执行
useEffect(() => {
const { username, password } = getUser();
// 这里如果不小心写成了setFieldValue会报错,差了个s,这个只能输入一个参数,有s的才能输入多个
form.setFieldsValue({ username, password });
}, []);
return (
<div className={styled.contain}>
<div>
<Space>
<Title level={2}>
<UserAddOutlined />
</Title>
<Title level={2}>用户登录</Title>
</Space>
</div>
<div>
<Form
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
onFinish={onFinish}
// 默认remember设置为true
initialValues={{ remember: true }}
// 将返回值关联起来,即使其变成受控组件
form={form}
>
<Form.Item
label="用户名"
name="username"
rules={[
{ required: true, message: "请输入用户名" },
// string表示按长度计算区间范围,不然变成数字就成最小5最大20了
{
type: "string",
min: 5,
max: 20,
message: "字符长度在5-20之间",
},
{
pattern: /^\w+$/,
message: "只能是字母数字下划线",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: "请输入密码" }]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
wrapperCol={{ offset: 6, span: 16 }}
>
{/* 表单需要有name和value才能提交,valuePropname就是将Checkbox的checked属性,即选中的属性(true || false)当作值 */}
<Checkbox>记住我</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 16 }}>
<Space>
<Button type="primary" htmlType="submit">
登录
</Button>
<Link to={REGISTER_PATHNAME}>注册新用户</Link>
</Space>
</Form.Item>
</Form>
</div>
</div>
);
};
export default Login;