React Hooks
手动创建react项目
npm init -y
npm install react react-dom
npm install --save-dev @babel/core babel-loader @babel/cli @babel/preset-env @babel/preset-react
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev html-webpack-plugin
创建src目录,index.js,public目录和index.html,.babelrc,webpack.config.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
webpack:使我们能够在项目中使用 webpack 的实际包
webpack-cli:允许我们在命令行中运行 webpack 命令
webpack-dev-server:Webpack 服务器将在开发环境中充当我们的服务器。如果您熟悉更高级别的开发服务器 live-server 或 nodemon,那么它的工作方式是相同的。
// index.js
import React from 'react'
import { createRoot } from 'react-dom/client';
import App from './src/App.js'
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App/>);
// App.jsx
import React from "react";
function App() {
return (
<div>
<h1>Hello React</h1>
</div>
)
}
export default App
创建webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { entry: "./index.js", mode: "development", output: { path: path.resolve(__dirname, "./dist"), filename: "index_bundle.js", }, target: "web", devServer: { port: "8080", static: { directory: path.join(__dirname, "public"), }, open: true, hot: true, liveReload: true, }, resolve: { extensions: [".js", ".jsx", ".json"], }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: "babel-loader", }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, "public", "index.html"), }), ], };
{
"presets": ["@babel/preset-env","@babel/preset-react"]
}
修改package.json
"scripts": {
"start": "webpack-dev-server .",
"build": "webpack ."
},
useState
// App.js
import React, { useState } from "react";
function App() {
const [count,setCount] = useState(0);
function add(){
setCount(count + 1)
}
function sub() {
setCount(count - 1)
}
return (
<div>
<h1>Hello React</h1>
<h3>{count}</h3>
<button onClick={add}>Add</button>
<button onClick={sub}>Sub</button>
</div>
)
}
export default App
那如果一次想加2呢?
import React, { useState } from "react"; function App() { const [count,setCount] = useState(0); function add(){ setCount(count + 1) setCount(count + 1) } function sub() { setCount(count - 1) } return ( <div> <h1>Hello React</h1> <h3>{count}</h3> <button onClick={add}>Add</button> <button onClick={sub}>Sub</button> </div> ) } export default App
我们这样写,点击Add发现还是每次只能加1,这是为什么呢?
这是因为React的setState是一个批量更新的过程,而不是立即更新。当您连续多次调用setState时,React会将多个setState调用合并到一次更新中。这样做是为了提高性能,避免不必要的重复渲染。
如果想在单次更新中多次增加count的值,可以使用函数形式的setState,它可以接收之前的state作为参数,并根据之前的state计算新的state。
import React, { useState } from "react"; function App() { const [count,setCount] = useState(0); function add(){ setCount((prevCount)=>prevCount + 1) setCount((prevCount)=>prevCount + 1) } function sub() { setCount(count - 1) } return ( <div> <h1>Hello React</h1> <h3>{count}</h3> <button onClick={add}>Add</button> <button onClick={sub}>Sub</button> </div> ) } export default App
这样就正常了
使用useState更新数组或者对象
如果直接修改原有的数组或对象,由于引用没有改变,React 将无法检测到状态的变化,从而导致组件无法正确更新。
React 倾向于函数式编程风格,这种风格强调不可变性和无副作用的纯函数。通过创建新的数组或对象,而不是直接修改它们,我们可以更好地遵循这种编程风格。
相反,使用不可变的方式来更新数组或对象,如使用
concat
、slice
、...
扩展运算符等,可以确保状态的不可变性,并且更易于推理和优化性能。这也是 React 官方文档中推荐的做法。
import React, { useState } from 'react';
function App() {
const [items, setItems] = useState(['apple', 'banana', 'orange']);
const addItem = () => {
setItems([...items, 'grape']);
};
const removeItem = (index) => {
setItems(items.filter((item, i) => i !== index));
};
return (
<div>
<h1>Fruit List</h1>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<button onClick={() => removeItem(index)}>Remove</button>
</li>
))}
</ul>
<button onClick={addItem}>Add Grape</button>
</div>
);
}
export default App;
import React, { useState } from 'react';
function App() {
const [user, setUser] = useState({ name: 'John', age: 30 });
const updateName = () => {
setUser({ ...user, name: 'Jane' });
};
const incrementAge = () => {
setUser({ ...user, age: user.age + 1 });
};
return (
<div>
<h1>User Details</h1>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={updateName}>Update Name</button>
<button onClick={incrementAge}>Increment Age</button>
</div>
);
}
export default App;
useEffect
useEffect
是 React 提供的一个钩子函数,用于在函数组件中执行副作用操作,例如数据获取、订阅、手动修改 DOM 等。它类似于 componentDidMount
、componentDidUpdate
和 componentWillUnmount
这些生命周期方法在类组件中的作用。
useEffect
是 React 提供的一个钩子函数,用于在函数组件中执行副作用操作,例如数据获取、订阅、手动修改 DOM 等。它类似于 componentDidMount
、componentDidUpdate
和 componentWillUnmount
这些生命周期方法在类组件中的作用。
useEffect 的语法
useEffect(effect, dependencies)
effect
是一个函数,它描述了要执行的副作用操作。它可以返回一个清理函数,用于在组件卸载或下一次effect执行之前清理一些副作用。dependencies
是一个可选的数组,用于指定 effect 依赖的状态或属性。只有当这些依赖项发生变化时,effect 函数才会被重新执行。如果不提供这个数组,effect 将在每次组件渲染后执行。
useEffect 的执行时机
- 初次渲染时,effect 函数会被执行一次。
- 在每次更新后,React 会先比较 effect 的依赖项是否发生变化。如果发生变化,effect 函数就会被重新执行。
- 如果 effect 函数返回了一个清理函数,那么在下一次effect执行之前或组件卸载之前,这个清理函数会被执行。
注意事项
- 不要在 effect 函数内部定义函数。这会导致每次渲染时都创建一个新的函数实例,从而影响依赖项的比较。相反,应该在组件函数作用域内定义并传递给 effect。
- 注意依赖项的顺序。依赖项数组中的值顺序发生变化也会导致 effect 重新执行。
- 避免在 effect 中执行昂贵的操作。如果需要执行昂贵的操作,可以考虑使用
useMemo
或useCallback
等钩子进行性能优化。
使用示例
-
模拟组件生命周期
import React, { useEffect } from 'react'; function MyComponent() { useEffect(() => { // 这里是副作用代码,会在组件渲染后执行 console.log('组件已挂载或更新'); // 清理函数,用于在组件卸载或更新前执行清理工作 return () => { console.log('组件即将卸载或更新,执行清理'); }; }, []); // 空依赖数组表示这个副作用只在组件挂载时运行一次 return ( <div> <p>这里是组件内容</p> </div> ); } export default MyComponent;
例子:
import React, { useEffect, useState } from "react";
export default function App() {
const [count,setCount] = useState(0)
useEffect(()=>{
console.log('====================================');
console.log("Component Mounted or Updated");
console.log('====================================');
},[count])
function add() {
return setCount((prevCount)=>prevCount + 1)
}
return (
<div>
<h1>HelloWorld</h1>
<h3>{count}</h3>
<button onClick={add}>Add</button>
</div>
)
}
我们在useEffect的第二个参数上写上我们要监控的值,当这个值发生变化的时候,传入useEffect的第一个回调函数就会被调用,然后再次打印
import React, { useEffect, useState } from "react";
export default function App() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
const [flag, setFlag] = useState(true);
useEffect(() => {
//第一次加载组件的时候,这里的内容会被执行
console.log("====================================");
console.log("Component Mounted or Updated");
console.log("====================================");
return () => {
//卸载组件的时候,这里的回调函数会被执行
console.log("组件即将卸载或更新,执行清理");
};
});
function handleSubmit(e) {
e.preventDefault();
console.log("====================================");
console.log("表单数据", formData);
console.log("====================================");
}
function handleChange(e) {
const { name, value } = e.target;
setFormData((prevFormData) => ({
...prevFormData,
[name]: value,
}));
}
return (
<div>
{flag && (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">姓名:</label>
<input
type="text"
id="name"
value={formData.name}
onChange={handleChange}
></input>
</div>
<div>
<label htmlFor="email">邮箱:</label>
<input
type="text"
id="email"
value={formData.email}
onChange={handleChange}
></input>
</div>
<div>
<label htmlFor="password">密码:</label>
<input
type="password"
id="password"
value={formData.password}
onChange={handleChange}
></input>
</div>
<button type="submit">提交</button>
</form>
)}
<button onClick={()=>setFlag(!flag)}>{flag ? 'Hide' : 'Show'}</button>
</div>
);
}
点击Hide前
点击后
- 订阅和取消订阅事件
import React, { useState, useEffect } from 'react';
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
// 清理函数用于取消订阅
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return (
<div>
<p>X: {position.x}, Y: {position.y}</p>
</div>
);
}
- 数据获取
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
return (
<div>
{data ? (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
}
通过这些示例,您可以了解到 useEffect
可以模拟类组件的生命周期、订阅和取消订阅事件、获取数据等常见操作。在使用 useEffect
时,请注意依赖项的设置和清理函数的使用,以确保正确的效果和避免内存泄漏。
useRef
useRef
是 React 中的一个钩子(Hook),它允许你在函数组件中创建一个可变的引用对象。这个引用对象在组件的整个生命周期内保持不变,即使组件重新渲染也不会改变。useRef
主要用于以下场景:
- 操作 DOM 元素:当你需要直接访问 DOM 节点时,可以使用
useRef
来创建一个引用,并将其附加到 DOM 元素上。 - 获取组件实例:在自定义组件中,你可以使用
useRef
来获取组件的实例。 - 跨渲染周期保持状态:
useRef
可以用来存储跨渲染周期的状态,因为useRef
的current
属性不会在渲染时更新。
基本用法
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
);
}
在这个例子中,我们创建了一个 inputRef
引用,并将其附加到 input
元素上。当用户点击按钮时,focusInput
函数会被调用,从而使焦点移动到输入框上。
注意事项
- 不要在渲染期间写入或读取
ref.current
:在组件的渲染逻辑中(即 JSX 部分),你应该避免直接修改ref.current
。这可能会导致不可预测的组件行为。相反,你应该在事件处理程序或useEffect
钩子中进行这些操作。 - 初始化
useRef
时避免重复创建:如果你在useRef
中创建一个对象,确保只在首次渲染时创建它,以避免不必要的重复创建。可以通过检查current
是否为null
来实现。 - 自定义组件的 ref 转发:如果你想将
ref
传递给一个自定义组件的内部 DOM 节点,你需要使用React.forwardRef
来转发ref
。
例子
操作 DOM
import { useRef } from 'react';
function TextInputWithFocusButton() {
const inputRef = useRef();
const focusTextInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusTextInput}>Focus the input field</button>
</>
);
}
跨渲染周期保持状态
import { useRef, useEffect } from 'react';
function UseEffectExample() {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
return <div>Example of using useRef with useEffect</div>;
}
在这个例子中,我们使用 useRef
来保存定时器的 ID,以便在组件卸载时能够清除定时器。
自定义组件的 ref 转发
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
const VideoPlayer = forwardRef((props, ref) => {
const videoRef = useRef();
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
},
pause() {
videoRef.current.pause();
},
}));
return <video ref={videoRef} src="video.mp4" />;
});
function App() {
const videoRef = useRef();
useEffect(() => {
videoRef.current.play();
return () => {
videoRef.current.pause();
};
}, []);
return <VideoPlayer ref={videoRef} />;
}
在这个例子中,我们使用 forwardRef
和 useImperativeHandle
来创建一个可引用的 VideoPlayer
组件,允许父组件控制视频的播放和暂停。
通过这些例子和注意事项,你应该能够更好地理解 useRef
的用法和在 React 函数组件中的实用场景。
useContext
import { useContext,createContext } from "react";
import "./App.css";
const MyContext = createContext();
function App() {
return (
<>
<MyContext.Provider value={{ someData: "Hello World" }}>
<ChildComponent />
</MyContext.Provider>
</>
);
}
function ChildComponent() {
const context = useContext(MyContext);
return <div>{context.someData}</div>;
}
export default App;
为什么使用 useContext
- 简化跨层级的数据传递:当你需要在嵌套很深的组件树中传递数据时,使用上下文可以避免在每个层级手动传递 props。
- 与状态管理库结合:上下文可以与状态管理库(如 Redux 或 MobX)结合使用,以便在组件之间共享全局状态。
- 自定义钩子:你可以创建自定义钩子来使用上下文,使得在多个组件中访问上下文值更加方便。
注意事项
- 避免过度使用:虽然上下文可以简化数据传递,但过度使用可能会导致难以追踪数据流和调试问题。在可能的情况下,尽量使用局部状态管理。
- 考虑性能:上下文的值变化会触发所有依赖于该上下文的组件的重新渲染。如果你的上下文值很大或者组件树很复杂,这可能会影响性能。
- 使用 React.memo 或 useMemo:如果你的组件依赖于上下文值,但本身不依赖于其他 props 或 state,可以使用
React.memo
或useMemo
来避免不必要的重新渲染。
useMemo
是 React 中的一个钩子(Hook),它帮助你优化组件性能,通过缓存计算结果来避免不必要的重复计算。当你有一个函数组件中的值是根据其他值计算得出的,并且这个值的计算成本较高或者结果不经常变化时,使用 useMemo
可以提高性能。
useMemo
基本用法
useMemo
接受两个参数:一个函数和一个依赖数组。函数返回的值会被缓存,并且只有当依赖数组中的值发生变化时,函数才会重新执行。
import { useMemo } from 'react';
function ExpensiveComponent(props) {
// 假设这是一个计算成本很高的函数
const result = doSomethingExpensive(props.input);
// 将计算结果缓存起来
const memoizedResult = useMemo(() => result, [props.input]);
return <div>{memoizedResult}</div>;
}
在这个例子中,doSomethingExpensive
函数的结果被 useMemo
缓存起来。只有当 props.input
发生变化时,doSomethingExpensive
才会重新执行。
注意事项
- 只有当函数的输出依赖于其参数时,才应该使用
useMemo
。如果你的函数不依赖于任何外部值,那么它可能不需要useMemo
。 - 不要将
useMemo
用于依赖于内部状态的值。如果你需要缓存基于组件内部状态的值,应该使用useState
。 useMemo
不是一个语义化的保证。React 可能会在内存不足等情况下丢弃缓存的值,所以useMemo
并不能保证缓存的值一定会被复用。useMemo
仅用于性能优化。如果你的代码在没有useMemo
的情况下也能正常工作,那么不应该仅仅为了使用useMemo
而使用它。- 考虑使用
useCallback
来缓存函数。如果你需要缓存一个函数而不是值,那么useCallback
可能是更合适的选择。
示例
缓存复杂计算结果
function CalculationComponent({ initialData }) {
// 一个复杂的计算函数
const calculateData = () => {
// ...复杂的计算逻辑
return complexResult;
};
// 使用 useMemo 缓存计算结果
const data = useMemo(calculateData, [initialData]);
return <div>Data: {data}</div>;
}
在这个例子中,calculateData
函数的结果被缓存,只有当 initialData
发生变化时,计算才会重新执行。
缓存函数
如果你需要缓存一个函数,而不是值,可以使用 useCallback
。但是,如果你的函数返回一个值,并且这个值依赖于函数的参数,你可以使用 useMemo
来缓存这个值。
function Component({ dependencies }) {
// 一个返回值的函数
const getValue = useCallback(() => {
let sum = 0;
for (const dep of dependencies) {
sum += dep;
}
return sum;
}, [dependencies]);
// 使用 useMemo 缓存函数的返回值
const memoizedValue = useMemo(() => getValue(), [getValue]);
return <div>Value: {memoizedValue}</div>;
}
在这个例子中,getValue
函数的返回值被 useMemo
缓存,以避免在每次渲染时都重新计算。
通过使用 useMemo
,你可以有效地优化组件的性能,特别是在处理计算成本较高的值时。记住,只有在值的计算依赖于组件的 props 或 state 时,才应该使用 useMemo
。