React 备忘单
该表包含所有 React v17 概念,从基础到高级,包括 Router 和 Redux + Toolkit。
- 你可以在我的GitHub上找到我的所有表格集合
基础知识
- React 使用基于组件的结构来对 HTML 进行分组和渲染
为什么使用组件?
-
DRY——不要重复自己=可重用性
-
关注点分离 = 不要在一个地方/功能上做太多事情
基本文件夹结构:
-
src/ -源文件夹包含所有基本文件
- index.js = 页面渲染的开始,第一个执行的文件
- index.css = 可以作为索引中的普通 css 文件导入,用于全局样式
- app.js = 以 JSX 呈现基本页面内容,在索引中导入并在 html 中代替根目录进行呈现
- components/ = 包含您要添加的组件的所有 javascript 文件,并将在 app.js 中导入 [大写命名]。必须只返回一个根元素。
-
public/ -包含最终在页面上呈现的内容,无需编辑
JSX——JavaScript XML:
- React 中使用的 HTML 代码被转换/转化为 js 代码来呈现 html 结构。
- JSX 中的单花括号可以包含 JavaScript 代码
{}
- 元素必须被包裹在 1 个父元素中才能返回
- JSX 在后台使用 React.createElement 和其他函数来渲染 HTML
初始化新的 React 项目
- 您也可以手动初始化项目,只需从 npm 包安装 react 即可
- 使用 create-react-app - 这将初始化一个基本项目,其中所有必需的包都已内置和设置,用于热重载、测试等。
- 可用于
npx create-react-app <app-name>
初始化第一个基本项目
- 可用于
- 现在,我们还可以使用vite.js,它是 create-react-app 的更好替代方案
React 组件基础知识
// MyComponent.js
import React from 'react'; // importing React from react is optional in sub files in latest React , because it is done in app js and inherited but doing it is good practise and to be on the safer side.
import './MyComponent.css'; // we can directly import a css file in component
function MyComponent(props) {
return (
<div className="my-component"> // instead of `class` in html `className` is used.
<h1>{props.title}</h1> // All Props are passed as an Object
<p>{props.description}</p>
<div className="content">
{props.children} // The Extra data/JSX passed can be accessed using props.children
</div>
</div>
);
}
export default MyComponent;
// App.js
import React from 'react';
import MyComponent from './MyComponent';
function App() {
return (
<div>
<MyComponent
title="My Title"
description="This is my description." > // Props/Attributes can be used to share data between components
<p>This is some additional content.</p> // We Can also simply pass extra data into components too
</MyComponent>
/>
</div>
);
}
export default App;
- 可以将复杂的 JSX 组分解为子组件(组合),保持组件较小的良好做法
州和事件
- 状态用于存储和管理随时间变化的数据,它允许组件跟踪自己的状态并相应地更新其 UI
- 它的可变的、本地声明的,即不能被其他组件访问,除非通过
- 我们还需要状态,因为更新正常值不会重新渲染页面,因此状态用于更新 jsx 并重新渲染数据。
- 每个组件中的每个状态都是单独处理的,没有联系,并且只影响其组件。
创建状态 - useState
- useState 是一个钩子 [钩子以“use”为前缀,并且始终用于功能组件],它允许创建变量、更改和更新变量,从而更新 DOM。
- 即使组件重新渲染,useState 的值仍然保持不变
// Counter.js
import React, { useState } from 'react'; // useState is imported directly from react library.
const Counter = () => {
// Declaring a state variable, initial value is used whenever the component is rendered for the first time.
const [count, setCount] = useState(0);
// useState returns 2 values that can be destructured.
// first is the variable itself and second is a function that allows to update the variable.
// Event handler for incrementing the count
const handleIncrement = () => {
setCount(count + 1); // Update the "count" state with a new value
// set Action schedules the update , it takes a small time to update the specified value.
// therefore you cant instantly use the value right after setting it as the updation might be delayed.
// in case of multiple set methods right after another they are batched together and executed together
};
// Event handler for decrementing the count
const handleDecrement = () => {
setCount(count - 1); // Update the "count" state with a new value
};
return (
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
};
export default Counter;
处理事件和表单
import React, { useState } from "react";
const FormExample = () => {
const [formData, setFormData] = useState({
firstName: "",
email: "",
});
// name of handler functions
const handleChange = (e) => {
// this function gets the data whenever it is called through the element
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const handleSubmit = (e) => {
e.preventDefault(); // we prevent the normal functionality of refreshing when form submits and add our logic.=
alert(
`Submitted Form Data:\nFirst Name: ${formData.firstName}\nEmail: ${formData.email}`
);
// Reset form fields after submitted
setFormData({
firstName: "",
email: "",
});
};
return (
<form onSubmit={handleSubmit}> // All the form events have "on" prefixed to them in react
<h1>Form Example</h1>
<label htmlFor="firstName">First Name:</label> // for is changed to htmlFor in React
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange} // we pass the function to be called on change
/>
<br />
<br />
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
<br />
<button type="submit" onClick={handleSubmit}> // onClick is used for Events on Buttons
Submit
</button>
</form>
);
};
export default FormExample;
useRef - DOM 操作
- useRef 主要用于直接从 JavaScript 进行 DOM 交互
import React, { useRef } from "react";
const MyForm = () => {
const inputRef = useRef(); // Ref for input field
const countRef = useRef(0); // Ref for mutable count value
const handleButtonClick = () => {
// Accessing DOM properties . here we are using it to just get values
console.log("Input value:", inputRef.current.value);
// Triggering DOM methods , triggering dom methods according to need
inputRef.current.focus();
inputRef.current.select();
// Storing mutable value , normally useRef should not be used to update values as it does not rerender the component.
// There it can be useful at place where only one value is needed to be change and rerender is not needed.
countRef.current++;
console.log("Current count:", countRef.current);
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleButtonClick}>Get Input Value, Focus Input, Increment Count</button>
</div>
);
};
export default MyForm;
- useRef 和 useState 的区别
- useState 可用于在用户输入或选择选项时存储和更新表单数据。当需要同时存储和更新时。
- useRef 可用于获取输入字段的引用,以便您可以直接访问它们的值或触发它们的方法。当不需要存储数据并且不想重新渲染组件时。
保存多个数据 - 对象状态
// We have 2 options , use 3 states to save different values or have just one state with object containing all 3 values
// Single states for name, age, and email = Multi State
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
// Object state for user = One State
const [user, setUser] = useState({
name: '',
age: 0,
email: ''
});
// Event handler for updating name
const handleNameChange = (event) => {
setName(event.target.value);
};
// That's the simple way
// Event handler for updating user object
const handleUserChange = (event) => {
// updating the object replaces it as a whole so we have to use the old object using spread operator
setUser({
...user,
[event.target.name]: event.target.value
});
// The issue here is we are using the old state, in case it is being updated at the same time we might get the wrong value for oldState because of scheduling
setUser((prevState) => {
...prevState,
[event.target.name]: event.target.value
});
// We solve the above issue , by using the function variation of setState which itself provides the UPDATED old value and will never be wrong. Especially helpful when running Asynchronously
};
- 我们也可以用同样的方式更新数组数据,使用扩展并传递旧值 -
setItems(prevItems => [...prevItems, newItem]);
传递数据 - 子级和父级
import React, { useState } from 'react';
// Child Component
const ChildComponent = ({ childData, onChildDataChange }) => { // A child component can recieve data in props
const handleInputChange = (event) => {
const newChildData = event.target.value;
onChildDataChange(newChildData); // data is saved in parent onChange
// a child component can also recieve functions from parent component, this can be used to send data to parents
// this concept is LIFTING STATE UP
};
return (
<div>
<h2>Child Component</h2>
<input type="text" value={childData} onChange={handleInputChange} />
<p>Child Data: {childData}</p>
</div>
);
};
// Parent Component
const ParentComponent = () => {
const [parentData, setParentData] = useState('Hello from parent!');
const handleChildDataChange = (newChildData) => {
setParentData(newChildData);
};
return (
<div>
<h1>Parent Component</h1>
<p>Parent Data: {parentData}</p>
<ChildComponent childData={parentData} onChildDataChange={handleChildDataChange} />
// Here parent is sending the data and a function to update data , to the child
// Passing data to child , this concept is PASSING STATE DATA
</div>
);
};
export default ParentComponent;
附加功能
React 组件的类型
- 无状态/表现型/哑/纯组件
- 纯组件是指对于相同的输入 props 始终产生相同的输出,且没有任何副作用的组件。主要用于渲染 UI 元素,不包含任何逻辑或状态管理。
-
有状态/容器/智能/不纯组件
- 另一方面,非纯组件是指对于相同的输入 props 可能产生不同的输出或具有副作用的组件。非纯组件通常用于管理状态、处理复杂逻辑、进行 API 调用或与 DOM 交互。
-
高阶组件(HOC)
- 函数将组件作为输入,并返回带有附加 props 或行为的新组件。HOC 用于复用组件逻辑,例如处理身份验证、处理数据获取或为多个组件提供通用功能。
-
受控组件
- 每当一个组件连接到某个状态,以便我们管理数据时,它就是一个受控组件。受控组件允许双向绑定,我们可以通过编程方式使用和更新其数据。
渲染列表和条件内容
// MyComponent.js - rendering lists
import React from 'react';
import DataItem from './DataItem'; // Import the child component
const MyComponent = ({ data }) => {
// here data provides us with an array of object of data that we want to render
const renderDataItems = () => {
// as jsx can automatically render element of arrays as html elements, we can directly return from map.
return data.map(item => (
// specifying a unique key for every component is very important as it helps react to identify items individually, and not update all the items when asked to. This prevents performance issues and unwanted bugs.
<DataItem key={item.id} item={item} />
// Render the DataItem child component with props , doing this helps to breakdown the logic and make it easily changeable and reusable.
// We should never use index as the key as it becomes inconsitent when data is changed.
));
};
return (
<div>
<h1>My Data Items:</h1>
{renderDataItems()} {/* we use a function to return the list of items we want to render */}
{/* Call the renderDataItems function to render the data items and not directly map here for a clean code */}
</div>
);
};
export default MyComponent;
// DataItem.js - Rendering Content Conditionally
import React from 'react';
const DataItem = ({ item }) => {
return (
<div>
<p>ID: {item.id}</p>
<p>Name: {item.name}</p>
{item.age && <p>Age: {item.age}</p>}
{/* && operator allows to Conditionally render age only if it exists, else its false and doesnt render */}
{item.gender ? <p>Gender: {item.gender}</p> : null}
{/* We can also use Ternary operator, but in most cases && does the work*/}
</div>
);
};
export default DataItem;
关于表单的更多信息
-
表单验证,可以通过三种方式完成
- 每次击键/值发生变化时 - 问题是在输入完成之前就会发出警告
- 当输入失去焦点时 - 问题仅发生在用户误点击并失去焦点时,他会收到错误
- 提交表单时 - 允许用户输入所有数据,但错误反馈为时已晚
-
我们可以根据需要使用特定方式验证表单。例如,对于最长的表单,每次按键都验证;对于中等大小的表单,每次提交都验证。可以根据需求进行验证。
import React, { useState } from 'react';
const FormValidationComponent = () => {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [errors, setErrors] = useState({
email: '',
password: '',
});
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleInputBlur = (e) => {
const { name, value } = e.target;
validateInput(name, value);
};
const validateInput = (name, value) => {
let error = '';
switch (name) {
case 'email':
if (!value) {
error = 'Email is required';
} else if (!isValidEmail(value)) {
error = 'Invalid email format';
}
break;
case 'password':
if (!value) {
error = 'Password is required';
} else if (value.length < 6) {
error = 'Password must be at least 6 characters';
}
break;
default:
break;
}
setErrors({ ...errors, [name]: error });
};
const handleSubmit = (e) => {
e.preventDefault();
// Perform form submission and additional validation if needed
};
const isValidEmail = (email) => {
// Validate email format, you can use a library or regex for more complex validation
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
return (
<div>
<h1>Form Validation Example</h1>
<form onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
onBlur={handleInputBlur}
/>
{errors.email && <p>{errors.email}</p>}
</div>
<div>
<label>Password:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
onBlur={handleInputBlur}
/>
{errors.password && <p>{errors.password}</p>}
</div>
<button type="submit">Submit</button>
</form>
</div>
);
};
-
仅有前端验证是不够的,我们应该始终进行后端验证,因为可以使用检查器绕过 javascript 验证。
-
我们可以使用自定义钩子来制作/验证表单中的多个输入,因为使用组件时状态将被单独处理,但使其成为钩子将允许我们将状态绑定到一个主表单组件。
-
我们也可以使用包来处理表单,例如 Formik 或 ReactHookForm
React 中的样式
-
在 JSX 中使用条件语句动态添加内联样式和类
-
直接导入 css 文件的一个问题是,如果一个文件导入到任何组件中,那么这些类也可以在任何其他组件中使用 = 避免这种情况的两种方法是
-
我们可以使用...向 JSX 添加样式。
style={{}}
破折号被删除,名称变为驼峰式命名法。- 样式化的组件
- 使用标记模板文字
- 和元素作为设置元素样式的方法
- 它们返回指定元素的样式组件,您可以在代码中使用它
- 我们可以将 props 发送给样式化的组件,并在其中动态渲染 CSS
- 媒体查询也可以在样式组件中使用
- CSS模块
- 它从你的 CSS 类中获取 CSS,并通过赋予它一个新的类名使它们变得唯一
调试
-
使用 Debugger = Goto Sources ,文件并添加调试器在您想要测试的任何地方停止[这将暂停代码,我们可以在需要时恢复它]
-
React DevTools = 获取有关正在使用和渲染的组件的信息,包括钩子和其他信息
片段、门户和引用
片段
- 用于分组 JSX,这是 JSX 的一个限制
-
我们不能在 JSX 中返回超过 1 个根元素。
-
我们可以解决这个问题,通过将所有内容包装在
-
div 或其他元素,创建一个 div soup = div 内部未使用的 div,只是为了解决这个问题
-
将返回的组件包装到数组中,因为 React 可以渲染相邻元素的数组(问题是你需要一个元素数组的键)
-
创建一个 Wrapper 组件,它只返回 props.children .. 并在相邻代码中使用它
-
Wrapper 默认为...
<React.Fragment>
或者简单地<></>
不渲染任何组件/代码并同时包装我们的代码......这些是React Fragments。
门户
-
Portal 通过重新排序元素来帮助获得更清晰的 DOM
-
模态框、叠加层、侧边抽屉在组件内部渲染,嵌套它们并不是一个好习惯,因为它们是叠加层
-
我们可以使用门户将组件保持在我们想要的任何位置...但在 html 中的其他地方呈现它
-
用法 -
-
在 index.html 中添加带有某些 Id 的元素,以便将组件传输到这些元素,您也可以在根目录之外进行渲染。
-
我们将使用 react-dom 来帮助渲染 react 的 dom,我们将从中导入 ReactDOM
-
对于要渲染到在 index.html 中创建的 ID 的组件,请使用
ReactDOM.createPortal(<component with props/> , document.getElementById('yourCustomElementId))
-
props 被传递给组件,因此请相应地使用它,即门户中的组件将 props 传递给原始组件
-
这与在 App.js 中渲染 root 的方式相同...但这次我们不是渲染而是传送它
参考文献
-
Refs 或引用允许我们访问其他 DOM 元素以访问数据
-
Refs 可以用来存储 DOM 组件,我们可以通过以下方式使用它
-
使用 useRef 创建变量...
const inputRef = useRef()
-
然后在 html 元素上传递一个键
ref = {inputRef}
-
现在我们可以使用这个 inputRef 来获取状态,并在代码中随时赋值我们添加的元素的所有属性。它总是给出一个带有当前键的对象来保存该元素。
-
使用 ref 的一个例子是,当按下提交按钮时,可以使用 直接读取输入,
inputRef.current.value
而不是使用键盘记录 onChange 到状态,并以相同的方式重置值 -
注意:最好不要使用 ref 来操作 DOM,而使用它来读取 DOM 的数据和元素。
-
当使用 Refs 访问输入值时,它是不受控制的……并且不受控制。因为在这种情况下我们不再设置输入的值/状态
副作用/钩子、Reducers 和 Context API
副作用 = 组件渲染之外的任务,例如 Http 请求、数据处理
useEffect
-
useEffect 每当指定的依赖项发生变化时执行一个函数
-
useEffect(() => {...}, [dependencies]);
-
通常,每次更新时都会执行组件中的开放代码,使用 useeffect 我们可以控制何时执行特定代码。
-
甚至无需指定任何数组,useeffect 就会在组件的每次状态更新时运行
-
如果 useeffect 中的依赖项数组为空,它将仅在组件渲染时启动并运行,并且由于没有提供依赖项,因此它不会再次运行,直到组件再次渲染。
-
为了使 useeffect 按需运行,我们可以简单地添加特定的依赖项,以便在任何指定的内容发生变化时监视和运行代码。
-
去抖动- 确保某些内容不会一次又一次地更新,而只在需要时更新一次。和节流。
-
清理函数 = 在 useeffect 第一次运行之前运行的函数...它也会在组件从 DOM 卸载时运行。对于清除不再使用的东西/数据或计时器很有用。
-
我们可以通过在 useEffect 中返回一个函数来创建清理函数。
useReducer
-
useReducer 对于复杂状态、多个状态以及一个状态依赖于其他状态的情况很有用(useState 的替代方案)
-
用法
const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
-
状态 = 我们正在使用的正常值
-
dispatchFn = 一个用于更新状态的函数,但我们传递了一个 Action 供 reducerFn 进行评估
- 动作可以是任何字符串或数字,但通常它是一个指定任务类型和有效载荷/值的对象=
{type: 'SAVE_VALUE', val: VALUE}
- 动作可以是任何字符串或数字,但通常它是一个指定任务类型和有效载荷/值的对象=
-
reducerFn = 运行 dispatchFn 时自动触发的函数,它接收最新状态并运行 Action,并返回更新的状态。
- 从反应和动作中获取最后状态作为参数
- 我们可以针对这个 reducer 内部使用的不同操作创建检查,并采取相应的操作
-
initialState = 是我们希望默认设置的值
-
initialFn = 是一个函数,我们希望在状态中设置它的返回值[例如 http 请求]
-
useEffect 和 useReducers
- 我们可以使用对象解构从 Reducer 中获取特定值并在 useEffect 中观察它们,而无需在 useEffect 中观察整个 Reducer。
-
useState 与 UseReducer
- useState = 基础数据管理,用于独立数据和短数据
- useReducer = 如果需要更多功能,特别是在数组和对象更新时,要注意我们使用最新状态,并且当多个状态相互关联时也适用于不同的操作。
上下文 API
- Context 是所有组件的后台断开的单一状态存储,允许在任何级别的组件之间共享数据。
-
在 src 中创建一个名为store的新文件夹
-
创建2个文件
- 名称上下文.js
import React from "react"; // this dosent have to be object, but using object gives access to manage multiple data const nameContext = React.createContext({ arr: [], amount: 0, addItem: (item) => {}, removeItem: (id) => {}, clearAll: () => {}, }); // this creates a component for the context that we will provide to our components and let them use it // this is just a prototype that lets vscode and react know that these will be value being used in the context export default nameContext;
-
nameReducer.js
import { useReducer } from "react";
import NameContext from "./name-context";
const defaultNameState = {
arr: [],
amount: 0,
};
const cartReducer = (state, action) => {
if (action.type === "ADD") {
//your logic
// can update the state directly here and action will contain the props sent with the dispatch
const newArr = state.arr.concat(action.item);
const updatedAmount = state.amount + 1;
return {
arr: newArr,
amount: updatedAmount,
};
}
// can add checks for other actions here
return defaultCartState;
// if the action is not specified default value will be returned.
};
const CartProvider = (props) => {
const [nameState, dispatchNameAction] = useReducer(
nameReducer,
defaultNametate
);
// we could also use useState if its not complex data
// we can also skip creating a state here and directly access context , but that brings the limitation to create local states
const addItemToNameHandler = (item) => {
dispatchNameAction({ type: "ADD", item: item });
};
const removeItemFromNameHandler = (id) => {
dispatchNameAction({ type: "REMOVE", id: id });
};
const clearNameHandler = () => {
dispatchNameAction({ type: "CLEAR" });
};
const cartContext = {
arr: cartState.arr,
amount: cartState.amount,
addItem: addItemToNameHandler,
removeItem: removeItemFromNameHandler,
clearCart: clearNameHandler,
};
return (
//this provider gives the access to all the children for the context
<NameContext.Provider value={nameContext}>
//the value prop gives access to all the children with the current updated value
{props.children}
</NameContext.Provider>
);
};
export default NameProvider;
- 现在用这个中间件提供程序包装app.js中的所有组件
<NameProvider>
//other components that need to use this store
// if we didnt create this middleware provider of ours , we would have had to add the NameContext.Provider directly where we want and manage a state at that place itself
// creating a custom provider helps to totally isolate all the functionality at one place.
</NameProvider>
- 要在组件中使用此上下文,我们将使用钩子useContext
import useContext from 'react';
import NameContext from '../store/name-context';
const compo = (props) => {
const nameCtx = useContext(NameContext);
// now this component will be automatically reevaluated whenever CartContext changes
// another to access context is Context.Consumer which needs a different and lengthy setup
// to access context data
const data = nameCtx.arr;
// we can update the state simply using the functions we created to dispatch data
nameCtx.addItem(item);
}
- 上下文限制 = 未针对高频变化/非常快速的更新按钮点击进行优化
钩子的规则
-
只在顶层调用钩子,而不是在嵌套函数/if 语句中调用钩子
-
可选 - 尝试将 useEffect 中使用的所有内容添加到依赖项中。
-
像 useEffect 这样的钩子也不允许在其中添加任何异步/承诺返回数据,我们可以通过创建一个带有异步的子函数或简单地在其中使用 .then 承诺来绕过它。
-
只能在函数/组件中使用[自定义钩子除外]
前锋裁判
-
前向引用是一个钩子,它允许您使用引用将功能从子级传递到父级,以访问特殊功能或值。
-
因为函数式组件无法访问 ref props
-
useImparativeHandle hook = 允许以编程方式调用组件中的某些内容
-
使用情况 =
useImperativeHandle(ref, () => {return {something: fnName}});
-
上面的 ref 是我们在组件中作为 prop 获取的参数 ref,并返回我们要使用的函数/ref 用法的对象值对。
-
现在要在 props 中使用 ref,因为 React 不允许我们在组件 props 中使用 =
const component = React.forwardRef((props,ref) => {...})
-
现在使用您在组件中传递的 ref,您可以在父级中使用 refPassed.fnName
-
我们还可以通过将内置组件的 ref 分配给 props ref 来简单地访问它,而不必使用命令式处理程序。
import { forwardRef, useImperativeHandle } from 'react';
const ChildComponent = forwardRef((props, ref) => {
const [state, setState] = useState(initialValue);
useImperativeHandle(ref, () => ({
reset: () => setState(initialValue),
increment: () => setState(state + 1),
}));
return <div>{state}</div>;
});
const ParentComponent = () => {
const childRef = useRef(null);
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={() => childRef.current.reset()}>Reset</button>
<button onClick={() => childRef.current.increment()}>Increment</button>
</div>
);
};
React 工作原理、useCallback 和 useMemo
-
我们创建的所有组件都经过 ReactDOM 并显示在 RealDOM 中
-
React 使用虚拟 DOM 来决定当前组件树的外观以及它应该呈现的外观。ReactDOM 接收这些差异,并相应地更新 RealDOM。
-
每当组件的 State/Props/Context 发生变化时,该组件就会重新执行
重新评估问题
-
这是一个组件问题,每次发生任何变化时组件逻辑都会运行
-
重新评估组件!==渲染 DOM
-
React 执行组件函数,然后根据差异仅更新 DOM 中需要更改的部分。
-
将上一个快照与当前快照进行比较,并仅更新特定元素。
-
每次重新评估时,所有子组件函数也会重新评估。
-
注意 - 重新评估而不是重新渲染
-
为了防止功能组件在每次运行时被重新评估,并且仅在 prop 更改时更新,我们可以使用以下方式包装导出的功能组件
React.memo
-
前任 -
export default React.memo(FuncCompo);
-
这将使组件仅在收到的 props 发生更改时更新,而不是在每次父级重新评估时更新
-
为什么不使用它?因为我们正在以不重新评估组件的性能成本与将其当前 props 与旧 props 进行比较的成本进行交易。
-
我们可以将其用于具有巨大逻辑的大型组件树,而这些逻辑不需要一次又一次地重新评估。
-
有一个问题,道具重新执行只适用于原始道具,而不适用于函数、数组等其他道具。
-
因为当父级重新执行时,所有内容都会重新创建,比较相同的原语会给出 true ,但没有两个函数或数组是相同的。
-
因此,如果这些是道具,即使它们没有改变,组件仍然会被重新评估。
使用回调
-
用于防止子组件在功能发生变化时重新评估
-
它允许我们将函数保存在内存中的一个位置,而不是在每次执行时重新创建它。
-
它将每个重新创建的函数指向相同的位置,这样它就不会被改变
-
要使用它我们可以简单地用 useCallback 包装一个函数
-
例子
const functionName = useCallback(() => {
//state updates or other code.
}, []);
-
UseCallback 也像 useEffect 一样接受一个数组作为依赖项,我们在其中添加那些将改变函数功能的状态。将其留空意味着无论发生什么变化,函数都将始终保持不变。
-
当有情况时,必须重新创建该函数,因为它依赖于来自函数外部的值,我们必须将其作为 useCallback 中的依赖项添加。否则它将继续使用原始存储的闭包值,并且永远不会因为 useCallback 而更新它。
状态和组件变化
-
useState 不会在每次重新渲染时改变值,因为 useState 是一个基于反应的钩子,它在第一次渲染时严格保存所有信息,并且除非组件被移除并再次添加到 dom,否则不会删除/重新创建它。 useReducer 也是如此。
-
状态更新和计划- 每次调用 setState 函数来更改状态的值时,该值不会立即更改,而是安排状态更新。这将对代码产生很大影响,特别是当有多个状态更改并且 React 优先考虑某些状态更改时
-
状态改变顺序始终被保留,并按顺序执行。
-
这就是为什么建议使用状态变化的函数形式,以便我们始终获得最新值
-
前任 -
setState(oldState => !oldState)
-
如果我们不这样做,我们就只能从上次重新渲染组件时获取状态值。
-
我们还可以使用除函数式方法之外的另一种方法,即通过将 useEffect 与我们正在使用的依赖项状态一起使用,以确保我们在每次状态更改时获得最新值
-
如果我们有 2 个连续的状态更新,并且 [回调/承诺] 之间没有延迟,例如将它们放在同一个函数中,它们将被批处理在一起并计为单个状态更新,仅导致一次重新渲染。
使用备忘录
-
这允许在重新评估时停止重新执行组件的特定部分
-
当我们使用 React.memo 时,我们可以控制整个组件的 props。但是,当有一个 props 需要同时更改时,一些性能要求高的功能虽然没有改变,但仍然会因为一个 props 的更改而执行。
-
我们可以使用 useMemo 来防止这种情况,它有助于记忆任何类型的数据,这意味着存储任何类型的数据并且不使其重新执行,就像 useCallback 对函数所做的一样。
-
它的使用方式与 useCallback 相同,第一个参数是一个返回性能繁重任务的匿名函数,第二个参数是依赖项数组,它允许在依赖项发生变化时运行任务并提供更新的值。
-
使用情况 =
const result = useMemo(() => {
return array.sort((a,b) => a-b);
}, [array]);
-
并将其留空意味着结果永远不会改变,也不必一次又一次地执行。在这里添加数组作为依赖项意味着每当数组发生变化时,它都会重新渲染,并且它在每次执行时都会发生变化,因为它不是原始的
-
因此,为了防止每次执行时都重新创建此数组,我们可以使用 useMemo 简单地创建数组的值
-
例如=
const arr = useMemo(() => [2,5,6,10,9], []);
-
现在数组将永远不会被视为改变,空依赖将确保它根本不需要改变并且将保持不变。
-
注意 - 如果需要,请仅将其用于性能密集型任务,因为使用它本身也会占用部分性能。
基于类的组件
-
通常我们使用函数式组件来定义返回 JSX 的组件
-
另一种方法是制作基于类的组件,该组件将具有返回 JSX 的 render() 函数。
-
例子 -
class Product extends Component {
render() {
return <h2>Product</h2>
}
}
-
如今,函数式方法已成为所有情况下的默认方法,过去(React < 16.8)状态管理组件需要基于类的方法,而现在它们仅用于错误边界的情况
-
React 16.8 引入了 hooks,它为函数式提供了基于类的功能
基本用法
-
基于类的组件可以从 Component 扩展,以渲染和访问 this.props
-
并且必须有一个 render() 函数,返回 JSX
-
基于类的组件可以与函数组件协同工作
类中的状态和事件
-
函数写在类中而不是渲染方法中
-
在基于类中,状态必须被命名为状态,并且只能是一个对象,而不能是其他东西。
-
例子
constructor() {
super()
this.state = {
name : "",
bool : true,
obj : {...}
}
}
-
状态必须始终在构造函数中初始化,并且需要调用 super,因为它是从 Component 扩展而来的
-
为了设置状态我们使用 this.setState();
-
this.setState({name: "Aj"})
对于基于类的情况,对象不会被覆盖而是被合并,因此即使我们只改变一件事,其他数据也会保留, -
我们可以用与 useState 相同的方式获取状态的旧值 -
-
this.setState((oldState) => {name : oldState.name + "12"})
-
我们可以在 render 方法中定义渲染特定 JSX 的辅助函数
-
在基于类中,我们必须使用状态,也必须使用 this.state.name
-
在 JavaScript 中传递函数时,我们必须使用“this”调用它们,并且如果我们在函数中使用它,则还要将类“this”绑定到它。
-
<button onClick={this.toggle.bind(this)}>
基于类的生命周期
-
基于类不使用钩子而是使用生命周期方法
-
componentDidMount() = 组件安装后调用 [评估并渲染] = useEffect(..., [])
- 这仅在组件第一次渲染时运行一次。
-
componentDidUpdate() = 组件更新/状态改变后调用 = useEffect(...,[someValue]);
- 在 didUpdate 函数 props 中,我们可以获取 prevProps 和 prevState 来比较特定的状态值并进行相应的更新
componentDidUpdate(prevProps, prevState){
if(prevState.value !=== currentValue){
//other code only executed if this value changed.
}
}
```
- componentWillUnmount() = Right before component is unmounted = cleanup in useEffect
### Context in Class based
- Class-based can use Context in almost the same way as functions, only change is how we use it , without useContext() and we can connect only one Context to a single classBased component.
### Error Boundaries - Best use case for class-based
- generally whenever we throw an Error at any place , the app crashes with error , or we use try catch
- For this we can create a new class based component , example named ErrorBoundary and give it a lifecycle method `componentDidCatch()` and this does not have any hook for now.
- `componentDidCatch(){}` this method is executed whenever any child throws an error.
- Example -
```javascript
class ErrorBoundary extends Component {
constructor() {
super();
this.state = {hasError: false}
}
componentDidCatch(error){
this.setState({hasError: true})
}
render() {
if(this.state.hasError){
return <p>Something Went Wrong</p>
}
return this.props.children;
}
}
- 现在我们可以将我们的组件包装在这个组件中,每当抛出错误时,它就会捕获并处理它们。
如何使用 Https 请求连接到后端数据库
-
由于安全条件和性能原因,FrontEnd/React 不直接连接到数据库,这就是为什么使用后端应用程序(node、php)作为 react 和 db 之间的中介。
-
发送请求可以通过两种方式完成
- Axios 包
- Fetch Api = 浏览器已提供
-
发送 GET 请求
fetch('url');
默认情况下是获取请求。然后我们会延迟收到响应,这就是我们可以使用承诺的原因。- 我们可以使用
fetch('url').then(response => {
return response.json(); //this is also delayed and sends a response
}).then(data => {
console.log(data.results);
});
```
- Async/Await
- It is a replacement of .then and .catch block , a javascript feature that we can simply use with react.
- We can simply handle loading states and errors to show loading data or error while requests are being made.
- Handling Errors
- 2xx codes = accepts
- 4xx / 5xx responsed might have to be handled
- We can catch errors using `.catch` for .then or try catch for async await
- **Fetch** dosent give an error for error status codes whereas axios can catch it.
- To check for errors in fetch , we have to check response.ok or response.status and throw error manually
- Sending POST request
- fetch takes a second argument with object where we can send post request and the data to be sent
```javascript
fetch('url', {
method: 'POST',
body: JSON.stringify(data), //this sends data as json
headers: {
'Content-type' : 'application/json' //tells that data is json
}
})
```
## Custom React Hooks
- Custom hooks are simply functions containing stateful logic , that can use other hooks and react state.
- We can use custom hooks when we have shared functionality between multiple components that are using hooks too
- Usage
- We should create a separate file for the customhook in a folder named 'hooks' in src, and a must rule is for the function to start with 'use' this tells react that it is going to use other hooks.
```javascript
const useCustom = () => {
//code with states and hooks that you want to use at multiple places
}
```
- Now we can simply use our hook based code in our custom hook and use that at the place that we want by simple calling it like a function.
- When we call a custom hook in a component the state and hooks of it are tied to that specific component, and not common between all components.
- as the custom hook is tied to the function it is used in , it will re-evaluate on every change of the custom hook too. that is why we can use useCallback & useMemo to save it from re-rendering if needed.
- we can return any state/number/variable from a custom component the the component that we want to use it in.
- we can simply pass arguments/parameters to the custom hook for different use cases.
- dont forget to update the inside hooks of custom hook , with dependencies of props
## Redux - State Management , App wide
- Local State - data in single component = useState and useReducer
- Cross-Component State - affects multiple components = uses props to transfer state [useState, useReducer]
- App-Wide State - state that effects entire app = prop chains , context , redux
- Why Redux in place of Context ?
- context has some disadvantages that redux can fulfill [therefore we can use both also too]
- context can become very complex because of multiple providers
- context is not that performant , not good for high frequency changes
- How does Redux Work
- Redux always has all the data at one central store [state]
- Components make a subscription to get the data directly from store
- To update the store data , we have Reducer Functions which mutate /change the data
- Components dispatch/trigger actions with descriptions which forward info to reducer and update the store.
### Basic Redux without React
Redux is based on observer pattern , which is the reason why it is able to do state updates as its able to observe all the changes in the state.
1. install - `npm install redux`
2. Create a reducer , with initial state and actions to perform
```javascript
const nameReducer = (state = {counter : 0}, action)=> {
if(action.type === "..."){
return {
counter: state.counter + 1,
}
}
//and so on state updates
}
```
- automatically called by redux, always recieves old/existing state and an action.
- it should always return a new state object
- it should be a pure function , without any calls / hooks
3. Create a store pointing at the reducer
```javascript
import {createStore} from 'redux'
const store = createStore(nameReducer);
```
4. Create a subcriber which gets triggered on every action and can get state
```javascript
const nameSubscriber = () => {
const latestState = store.getState();
}
```
- it is a function does not get any parameter , but can get the state in this because it gets triggered everytime a state update occurs.
5. subscribe your subscriber to the store
```javascript
store.subscribe(nameSubscriber);
```
- this function redux calls everytime state update occurs
6. now simply dispatch your actions to the store to perform actions on state
```javascript
store.dispatch({type: 'increment'});
```
- this calls reducer to run again and that calls the subscriber.
### Using Redux With React
- `npm install redux react-redux` to install redux in the project
- Create a folder "store" in your src for redux
- Create a file to create your store. ex-index.js
```javascript
import {createStore} from 'redux'
// Create a reducer , with initial state and actions to perform
const nameReducer = (state = {counter : 0}, action)=> {
if(action.type === "..."){
return {
counter: state.counter + 1,
}
}
//and so on state updates
return state;
}
//automatically called by redux, always recieves old/existing state and an action.
// it should always return a new state object
// it should be a pure function , without any calls / hooks
const store = createStore(nameReducer);
//this helps us access the specific state.
export default store;
- 接下来,我们需要用提供程序包装应用程序,以便应用程序的所有组件都可以访问商店
//in app.js
//other imports ...
import {Provider} from 'react-redux'
import store from "./store/index";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
- 接下来,在我们想要使用 store 的组件中,我们需要导入一个钩子 useSelector
import {useSelector} from 'react-redux';
const compo = () => {
const counter = useSelector(state => state.counter);
//like this we can get specific state data we want too
//useSelector manages creation of subscription for state updates automatically
// it creates a subscription when component is mounted and updates the data on its own
// it also unsubscribes when the component is removed from DOM
//... other code
// can simply use the counter data normally we will get updated on its own
}
- 为了在 React Redux 中更新此状态,我们使用 useDispatch hook,它将帮助我们将操作发送到 Store Reducer
const dispatch = useDispatch();
//this returns a dipatch function with which we can call the redux store
const increment = () => {
dispatch({type: 'increment'});
}
Redux 与基于类的组件
-
使用 useDispatch 和 useSelector,react-redux 还提供了另一个名为connect的函数,用于将基于类的组件与 redux 连接起来。
-
连接需要 2 个参数 =
- 首先是一个函数mapStateToProps,它将 redux 状态作为参数,并将键作为 props 传递给组件
- 第二个是另一个函数mapDispatchToProps,它将 dispatch 作为参数,在这个函数中,我们可以将函数作为 props 发送到组件并调用 dispatch 操作
-
用法
import {connect} from 'react-redux';
class Counter extends Component {
//we can use the data sent in props props
incrementHandler() {
this.props.increment(); //accessing the dispatch function
}
value = this.props.counter; //accessing the value
//other code...
}
const mapStateToProps = state => {
return {
counter: state.counter
// accessing the redux state , keys are passed as props
}
}
// function names can be anything , but this is conventional
const mapDispatchToProps = dispatch => {
// this function will recieve the dispatch
return {
increment: () => dispatch({type: 'increment'}),
decrement: () => dispatch({type: 'decrement'})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
将有效载荷附加到操作
- 我们可以在组件调度时传递数据,从而简单地在动作中的 Reducer 中获取数据
// in the reducer simply access the value using action
const nameReducer = (state = {counter : 0}, action)=> {
if(action.type === "ADD"){
return {
counter: state.counter + action.amount,
}
}
}
// now add the data while dispatching from the component
dispatch({type: 'ADD', amount: 5});
// this amount will be accessible in the reducer
使用多个状态属性
- 在 Reducer 中拥有多个状态,并针对性地使用它们
// in the file with your reducer
// creating an overall initial state
const initialState = {
counter: 0,
showCounter: true
}
const counterReducer = (state = initialState, action) => {
if(action.type === 'ADD') {
// never update the existing state directly because it is a reference , it can work but it can cause unforeseen issues
return {
counter: state.counter + 1,
showCounter: state.showCounter
// we are having to specify the full state because react dosent merge it instead it just updates the overall state with what we return
// always return a brand new object with all the properties
}
}
if(action.type === 'toggle'){
return {
showCounter: !state.showCounter,
counter: state.counter
}
}
return state;
}
const store = createStore(counterReducer);
export default store
- 现在我们可以使用我们想要的组件中的 useSelector 简单地访问这些值。
Redux 挑战
-
必须记住所有动作名称
-
大型国家物体集中在一个地方用于更大的项目
-
每次发生变化时都必须重新返回整个状态
-
所有这些问题都可以通过手动小修复来解决,但更好的方法是使用 Redux Toolkit
Redux 工具包
-
它是由 redux 的同一个团队制作的,使事情更容易使用,而不是必须使用
-
安装 -
npm install @reduxjks/toolkit
,您现在可以从包中删除 redux ,因为它已经有了。
如何在 Toolkit 中创建切片
-
在我们创建 Reducer 的文件中,例如
store/index.js
-
我们在@reduxjs/toolkit中拥有createReducer和createSlice[更强大]
-
createSlice = 我们准备一个全局状态的切片,其中包含不相连且碎片化的数据
import {createSlice, configureStore} from '@reduxjs/toolkit';
// 1. Create slice for the state you want
// createSlice wants an object as argument
const counterSlice = createSlice({
name: 'counter', //any custom name that you wish,
initialState : {counter: 0, showCounter: true}, // this gives it an initial value to start with
reducers: { // this takes all the reducers we need to perform state updates
increment(state){ // we dont need if checks here , cause we can directly access these
state.counter++; // toolkit uses immer which automtically mutates and does the update , so we can directly work with the state without having to copy it
},
increase(state,action){
state.counter += action.payload; // toolkit automatically attaches any data in a property names payload which cannot be changed
}
}
})
const initialAuthState = {
isAuthenticated: false,
}
const authSlice = createSlice({
name: 'authentication',
initialState : initialAuthState,
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
}
}
})
// 2. To let the store know about the slice and reducer to use
const store = createStore(counterSlice.reducer);
// this is good if we have just one slice but when we are working with multiple slices
// 2. We can use configureStore in toolkit, alternative to combineReducers of redux to send multiple reducers in state.
const store = configureStore({
// reducer : counterSlice.reducer // this is in case of one main reducer
reducer : {
counter : counterSlice.reducer,
auth: authSlice.reducer
}
// this can be used for multiple slices, these slices will be merged together and used in store automatically
});
export default store;
在 Toolkit Slice 中调度操作
- createSlice 自动将 Reducer 函数分配给切片,作为我们可以访问方法的操作
counterSlice.actions.increment();
// toolkit creates action objects when we call these reducers ,
// which are unique by default, that is why these are also called action creators
// these returned objects will already have uniques properties automatically being handled behind the scenes.
// that is why we dont have to worry about creating unique actions and adding if else
-
为了访问这些操作,我们还必须导出它们,然后导入到我们想要使用它们的地方
-
在创建 Reducer 的文件中...示例
store/index.js
export const counterActions = counterSlice.actions;
export const authAction = authSlice.actions;
- 现在在您想要使用这些的组件中,我们调度这些操作返回的唯一对象
import {counterActions} from '.../store/index';
import {useSelector , useDispatch} from 'react-redux'
const Counter = () => {
// const counter = useSelector(state => state.counter);
// if single slice is in store, we can also simply access our redux state in the same old way
const counter = useSelector(state => state.counter.counter);
// we have to specify the key of that slice when multiple are set
const authenticated = useSelector(state => state.auth.isAuthenticated);
const dispatch = useDispatch();
const incrementHandle = () => {
dispatch(counterActions.increment());
}
const increaseHandle = () => {
dispatch(counterActions.increase(10));
// example dipatches {type: UNIQUE_IDENTIFIER, payload: 10}
// this value is automatically stored in payload and can be access only as payload
// we can also send any data , array object anything in place of 10 and access as payload
}
}
拆分工具包代码
-
我们可以将切片分成商店中的不同文件
-
导出切片......
export default authSlice
我们也可以只导出减速器 -
然后将其导入到索引中的商店
-
import authReducer from './auth'
-
还将具体操作写入其特定文件中。
高级工具包
使用 Redux 处理异步任务和副作用
-
Reducer 有一个规则,即它们必须是纯粹的、同步的、没有副作用的
-
这就是为什么不允许在我们的 redux reducer 中添加异步函数
-
现在我们有两个选项来处理副作用和异步任务
- 在组件的 useEffect 等中写入并仅调度结果
- 创建我们自己的动作创建器,而不是使用工具包默认的动作创建器
直接向组件添加异步调用
-
不建议在数据转换过程中直接在组件中添加异步调用,因为
- 因为在将组件发送到后端之前,我们已经在组件中添加了所有与 Reducer 完全相同的逻辑
- 我们不能像在 Reducer 中那样改变状态,因为组件中没有使用工具包
-
减脂剂 vs 减脂成分 vs 减脂作用
-
同步和纯代码 = 优先使用 Reducer,避免使用 Action Creator 和组件
-
异步代码和副作用 = 优先使用 Action 创建器和组件,避免使用 Reducer 进行工作
另一个好方法是使用组件中的 useEffect 创建异步调用
.我们使用购物车的 UseSelector 获取数据
. 观察购物车中的变化并使用 useEffect 添加异步调用
。因此每次购物车发生变化时,useEffect 都会执行异步调用
。我们还必须检查它是否是第一次调用,因为当第一次呈现 App.js 时/创建购物车时也会调用 useEffect。
使用 Action Creator Thunk 添加异步调用
-
现在,当我们使用工具包提供的 Reducer 时,工具包会自动使用 Action Creator
-
要手动创建动作创建者 - 我们使用 Thunk
-
Thunk 是什么
- 延迟执行操作的功能
- 动作创建函数,它不直接返回动作数据,而是返回动作的另一个函数
-
使用 Thunk
- 转到切片创建的末尾,创建一个异步 thunk 中间件
const cartSlice = createSlice({...}) export const sendCartData = (cartData) => { // return {type: '', payload: 0} // this was done by toolkit automatically return async (dispatch) => { // now we can use async await here because we are not in reducer yet and we are performing actions before sending the data to the reducer dispatch(cartActions.setStatus({status: "Fetching Data..."})); const response = await fetch('url', {...}); if (response.ok) dispatch(cartActions.setStatus({status: "Fetching Complete}); else dispatch(cartActions.setStatus({status: "Error Occured"}); } }
- 现在在购物车中观看 App.js 中的 useEffect
useEffect(() => { if(isInitial){ isInitial = false; retun; } dispatch(sendCartData(cart)); //this sends the updated cart info // Redux toolkit allows us to send a function as an action which returns another function // Toolkit itself handles the function action and give it the access to dispatch in it too , for performing actions there // redux will go and call the function we made in slice itself }, [cart,dispatch]); //vs code will suggest to add dispatch here , adding it is no issue as it stays as the same function everytime and wont trigger the useEffect
- 随着 thunk 操作的增加,我们可以创建一个单独的文件 cart-actions.js 来存储所有这些 thunk 函数。
Redux 开发者工具
-
我们可以简单地在浏览器中安装 ReduxDevTools 作为扩展
-
如果我们在没有 reduxToolkit 的情况下使用 redux,我们必须进行设置才能使 DevTools 正常工作,但使用工具包时无需进行任何设置,开箱即用
-
通过这个我们可以看到所有被调度的动作..以及 redux 的所有数据更新。也可以在浏览器中手动控制它
路由器
-
改变了渲染/获取多个 HTML 页面的传统路由,而是通过有条件地检查页面路由来使用 React 渲染 HTML 内容。
-
帮助我们做到这一点的第三方包是 react-router
-
安装
npm install react-router-dom
基本用法
- 使用 BrowserRouter 组件包装你的应用程序,以激活 index.js 中的 React Router 功能
import {BrowserRouter} from 'react-router-dom'
//...
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
- 在 app.js 中导入 Route 并相应地设置组件的路由
import {Route} from 'react-router-dom';
//import Home component
//import other components
// as above components will be loaded as pages , we can store them
// in a different folder named pages or screens.
function App() {
return (
<div>
<Route path="/"> //yourDomain.com/
<Home/>
</Route>
<Route path="/other"> //yourDomain.com/other
<Other/>
</Route>
</div>
)
}
export default App;
使用链接和导航链接
-
我们不能简单地设置链接来重定向到路线,因为这会刷新页面并重置所有信息。
-
为此,react-router 具有 Link 组件,用于路由链接,防止浏览器默认,持久更改并防止刷新
-
基本链接的使用 -
import {Link} from 'react-router-dom';
const Header = () => {
return (
<header>
//nav ul li ...
<Link to='/home'>Home</Link>
<Link to='/products'>Products</Link>
</header>
)
}
- Link 不会给元素提供任何类,react-router 还提供了一个 NavLink 组件,允许我们向活动链接添加类
import {NavLink} from 'react-router-dom';
// ... imports , component data ... in the component
<NavLink activeClassName={classes.active} to='/home'>
Home
</NavLink>
动态路线和参数
- 在呈现项目列表并重定向到每个项目的详细信息页面的情况下,我们可以为每个产品创建具有动态路由的页面并将参数传递给它。
<Route path='/item-details/:itemId'>
<ItemDetails/>
</Route>
// this means we can load , domain/item-details/anything
// whatever we write in anything , /item1 , /p2123w
// will redirect us to the item details page
- 我们可以使用钩子检索路由中传递的自定义详细信息
useParams
。在我们加载的详细信息页面中=
import {useParams} from 'react-router-dom'
const ItemDetail = () => {
const params = useParams();
console.log(params.itemId);
//we can access multiple custom params using this key that we provided in the route
// example /:itemId/:itemPrice
}
-
当多个路由与当前路径匹配时,所有路由都将被加载。例如 - /products 将列出产品,而 /products/id 将呈现特定产品。但两者将同时呈现,因为它们都匹配 /products
-
React 路由器从上到下进行匹配,从路径的开头而不是整个路径进行匹配。
-
为此,我们有
Switch and Exact
import { Route, Switch } from 'react-router-dom';
// Switch makes it that only one of the matched routes are rendedered and not all
// but because of router functionality , the first one in order will be rendered which is not always right
// for that we can use exact which will only let the route render if its exactly the same.
<Switch>
<Route path='/welcome'>
<Welcome />
</Route>
<Route path='/products' exact>
<Products />
</Route>
<Route path='/products/:productId'>
<ProductDetail />
</Route>
</Switch>
- 我们还可以在路由中使用动态 JavaScript
嵌套路由和重定向
-
React Router 允许我们在任何组件的任何地方创建路由
-
我们可以在组件内部定义路由,并在组件加载时仅在其中渲染特定的数据
<section>
<h1>The Welcome Page</h1>
<Route path="/welcome/new-user">
<p>Welcome, new user!</p>
</Route>
</section>
// this will allow us to load the paragraph when , the new user route is added to welcome page.
- 我们还可以通过重定向将用户重定向到我们想要的页面
import {Redirect} from 'react-router-dom';
// exact is needed or else route will always match and loop it.
<Route path='/' exact>
<Redirect to='/home'>
</Route>
附加功能
- 我们可以为所有路线内容使用的基本布局创建一个自定义布局组件......并将我们的条件路线数据包装在其中。
<Layout> //containing navbar and props.children to render these inside components
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/other"><Other/></Route>
</Switch>
</Layout>
-
我们还可以简单地在任何地方使用 Route,就像 if else 一样,根据当前路线呈现动态内容。
-
未找到页面,创建未找到的 URL 捕获
// add a route at the end of all your routes already created that
// will catch all the remainging url and render not found page
// this will work because react matches routes from top to bottom
<Switch>
<Route path="/home"><Home/></Route>
// after all the routes at end add
<Route path="*">
<NotFoundPage />
</Route>
</Switch>
- useHistory是 react-router 提供的一个钩子,它允许我们使用 JavaScript 以编程方式浏览页面,而不仅仅是通过 Link/NavLink
import { useHistory } from 'react-router-dom';
const Component = () => {
const history = useHistory();
history.push('/other'); // sends to next page and allows the user to go back as it is pushed in history
history.replace('/other'); // sends to the next page and dosent allow the user to go back to previous page
}
- 提示组件用于在用户离开页面/填写表单时意外按下返回按钮时向用户发出警报。
import { Prompt } from 'react-router-dom';
<Prompt
when={isFillingForm} //if this this true then the user will be prompted if he tries to leave the page
message={(location) =>
'Are you sure you want to leave? All your entered data will be lost!'
}
/>
- useRouteMatch提供基于内部 react-router 的当前路由信息,useLocation提供浏览器级别的当前路由信息
const match = useRouteMatch();
const location = useLocation();
// match 不仅包含 url,还包含我们创建的参数 :itemId 和参数
// location 仅包含 url 和路由
// 在嵌套路由中使用匹配当前页面 URL
// 允许我们仅在第一级管理路由,所有其他路由将自动更新
- React router 还提供了一种自定义的方式来编写 url ,不仅可以使用字符串,还可以通过
history.push(`${location.pathname}?sort=${(Ascending ? 'asc' : 'desc')}`);
//can be written as
history.push({
pathname: location.pathname,
search: `?sort=${(Ascending ? 'asc' : 'desc')}`
});
查询参数
-
当我们只需要传递数据而不是将其作为路由的一部分时,只需将其放在 url 中即可共享。
-
useHistory可用于向当前页面添加查询参数
-
useLocation可用于通过查询参数获取当前路径名。
import {useLocation, useHistory} from 'react-router-dom';
// in the component
const history = usehistory();
const location = useLocation();
history.push('/page?sort=asc');
// this will still rerender the current component even if we are in it
const queryParams = new URLSearhParams(location.search);
// this is built in constructor class of browser, which will return our query params as object
const sortAsc = queryParams.get('sort') === 'asc';
// now we can simply use the current param value
版本 6 的变更
-
视频参考 = React Router 6 - 变更内容及升级指南
-
交换机被路由取代
-
路线现在是自封闭的,并且组件现在通过元素属性传递
<Route path='/home' element={<Home />} />
-
选择路线的内部逻辑已经改变,不再需要精确,如果 URL 正是所写的,路由器 V6 会自动加载该 URL。
-
现在路由器还会自动选择最佳路径
name/*
即使它在name/:id
原因之前声明,也不会加载,我们已手动指定我们期望在其后显示文本name/edit
即使name/:id
在它之前声明了 ,也会被加载。现在路由器会查找最适合它的精确匹配。
-
NavLink activeClassName 属性已被删除,现在我们必须手动查明链接是否处于活动状态,我们可以通过以下方式做到这一点
// className can take a function in Navlinks now which will give the nav data to it.
<NavLink className={(navData) => navData.isActive ? classes.active: ''} to='/home'>
Home
</NavLink>
```
- **Redirect** is replaced with Navigate
```jsx
<Route path='/' element={<Navigate to='/home'/>} />
// this will push the next page into history
<Route path='/' element={<Navigate replace to='/home'/>} />
// this will work exactly like the old redirect and replace the history
```
- Now nested **Route** also need to be wrapped in **Routes**
- Because of new logic of choosing routes , to access nested routes
1. we have to write `/home/*` in the main route to catch all the nested routes inside.
2. in the inner component containg the nested route , we no more have to write `/home/nest` instead normally `nest` in the path and react will understand it because now it is relative.
3. This also effects the **Links** inside components , their path is also relative now.
- Another option to make nested routes , is by adding Route children in the main app.js
```jsx
// in app.js
<Route path='/home/*' element={<Home/>}>
<Route path='other' element={<Other />} />
</Route>
// to define where the nested content has to be rendered , in home.js
// A new component OUTLET is available and will be simply replaced with the nested content
<section>
<Outlet/>
<h1>Hi</h1>
</section>
```
- **useHistory** is no longer there in v6 and is replaced with **useNavigate**
```js
const navigate = useNavigate();
navigate('/home'); // will push into history
navigate('/home', {replace: true}); // will redirect/replace in history
// forward and backward navigation is also possible by
navigate(-1) // backward
navigate(1) // forward
```
- **Prompt** component also no more exists in v6
- For even more latest and better changes of v6.4 = [React Router 6.4 - Getting Started](https://www.youtube.com/watch?v=L2kzUg6IzxM)
## Deployment
- Before deployment steps
1. Test Code
2. Optimize Code - and add Lazy Loading
3. Build App for production
4. Upload Production Code to Server
5. Configure Server
### Adding Lazy Loading
- Most of the bundles are download on first render of the website.
- Lazy loading is divinding the code into chunks and only downloading them when speicific route/functionality needs it.
- To use it
```js
import React from 'react';
const NewPost = React.lazy(() => import('./pages/NewPost'));
// this will make the app to not import this component right when the app is loaded
// instead the component will only be downloaded when it is being rendered
<Route path='/new-post'>
<NewPost /> // the component is downloaded only when this URL is hit.
</Route>
-
这就引发了一个问题:页面加载时,需要一些时间来下载组件。因此,React 需要一个备用 UI 来同时显示。
-
为此,我们有Suspense组件
-
我们包装所有代码,使用延迟加载并定义同时要显示的内容
import {Suspense} from 'react';
<section>
<Suspense fallback={<p>Loading...</p>}> // can define any jsx in here
// other elements/components inside
</Suspense>
</section>
构建并上传生产环境
-
create-react-app 已经有脚本,npm run build 在 package.json 中,它将把我们所有的代码最小化/优化到一个构建文件夹中,我们可以在任何我们希望的服务器上部署它。
-
我们还可以直接在 netlify 上部署我们的 react 项目,使用 github 存储库并将其设置为运行 build 命令本身并使用 build 命令进行生产,这也允许我们获得 CI/CD。
-
我们也可以仅通过部署我们的构建文件夹来使用任何其他静态托管提供商。
服务器端路由和客户端路由
-
在 React 中,我们创建单页应用程序。所有路由和 URL 均由主 URL 提供的 js 处理。因此,我们只使用一个 URL。
-
对于多页应用程序,通过访问不同的 URL 来呈现不同的页面,因此不需要任何设置。
部署 React 代码的一些地方
-
Netlify-免费
-
Vercel - 免费
-
Firebase
-
赫罗库
-
AWS
React 中的测试
-
手动测试 - 编写代码并立即测试,这种方法容易出错,因为我们无法手动测试所有场景和组合
-
自动化测试 - 我们编写的代码将自动多次逐一运行并测试项目的所有构建块
- 单元测试 - 单独测试最基本的组件/功能[最常用/重要]
- 集成测试——测试模块之间的组合和工作
- 端到端测试——测试用户流程的完整场景,可以手动完成,并且主要通过集成和单元测试本身完成
工具
-
运行测试,例如 Jest
-
用于模拟屏幕上的事物,反应测试库
-
两者均可在 create-react-app 中开箱即用
编写测试
-
安排-设置测试数据、条件和环境
-
Act——运行要测试的逻辑(函数)
-
断言——比较执行结果与预期值
例子
- 创建示例组件
//Greeting.js
import React from 'react';
function Greeting({ name }) {
return (
<div>
<h1>Hello, {name}!</h1>
</div>
);
}
export default Greeting;
- 现在为组件编写测试
//Greeting.test.js
import React from 'react';
import { render,screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message with the provided name', () => {
//test funtion is offered by jest , and here we write the test name
const { getByText } = render(<Greeting name="John" />);
// we render the component on screen
const greetingMessage = screen.getByText(/Hello, John!/i);
// now we select the component
expect(greetingMessage).toBeInTheDocument();
//expect helps us asser the resulta and check if the check has passed
});
- 现在我们可以使用
npm test
测试套件
- 我们可以将多个测试分组到一个测试套件中,以便将测试组织在一起
describe('Button component', () => {
test('displays the button label', () => {
const { getByText } = render(<Button label="Click me" />);
const buttonElement = getByText(/Click me/i);
expect(buttonElement).toBeInTheDocument();
});
});();
});
- 在这里,我们可以将多个测试放入 describe 函数中,将它们组合成一个套件
用户事件测试
- 我们可以通过以下方式模拟用户事件并运行相应的测试
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm component', () => {
test('disables the submit button until both fields are filled in', () => {
render(<LoginForm />);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /submit/i });
// Verify that the submit button is disabled initially
expect(submitButton).toBeDisabled();
// Simulate user input in the username field
userEvent.type(usernameInput, 'johndoe');
// Verify that the submit button is still disabled
expect(submitButton).toBeDisabled();
// Simulate user input in the password field
userEvent.type(passwordInput, 'password123');
// Verify that the submit button is now enabled
expect(submitButton).not.toBeDisabled();
});
});
集成测试
- 用于渲染组件的 render 函数,自动运行组件中使用的子组件并执行所需的检查
异步测试
-
对于异步、承诺,我们可以使用“screen.find”代替“screen.get/query”,因为它返回一个承诺。并且在可以指定的特定时间段内多次运行测试。
-
为了成功使用 find 函数,我们还必须将测试函数转换为异步函数并等待“screen.find”完成测试。
模拟请求
- 由于我们不应该在测试时向实际服务器发送请求,因为这会将无效数据发送到实时服务器,因此我们有两个选择
-
根本不发送请求,只检查我们的本地代码,看看组件是否崩溃
-
向虚假数据库发送请求以进行测试
。为了跳过发送请求,我们可以用模拟函数替换获取
- 例子 -
import React from 'react';
import { render, screen } from '@testing-library/react';
import UserList from './UserList';
describe('UserList component', () => {
test('fetches and displays a list of users', async () => {
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
];
//this replaces the fetch in the window , with a mock function
// proviced by jest, and then create our own response to work with
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockUsers),
})
);
render(<UserList />);
expect(screen.getByText(/user list/i)).toBeInTheDocument();
const userListItems = await screen.findAllByRole('listitem');
expect(userListItems).toHaveLength(2);
// Verify that fetch was called with the correct URL
expect(fetch).toHaveBeenCalledWith(
'https://jsonplaceholder.typicode.com/users'
);
});
});