React Hooks
手动创建react项目
仓库地址 https://github.com/meowrain/Manually-React-Project
https://dev.to/ivadyhabimana/how-to-create-a-react-app-without-using-create-react-app-a-step-by-step-guide-30nl
1
2
3
4
5
6
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
1
2
3
4
5
6
7
8
9
10
11
12
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 >
1
2
3
webpack:使我们能够在项目中使用 webpack 的实际包
webpack-cli:允许我们在命令行中运行 webpack 命令
webpack-dev-server:Webpack 服务器将在开发环境中充当我们的服务器。如果您熟悉更高级别的开发服务器 live-server 或 nodemon,那么它的工作方式是相同的。
1
2
3
4
5
6
7
8
// 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 /> );
1
2
3
4
5
6
7
8
9
10
// App.jsx
import React from "react" ;
function App () {
return (
< div >
< h1 > Hello React < /h1>
< /div>
)
}
export default App
创建webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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" ),
}),
],
};
1
2
3
{
"presets" : [ "@babel/preset-env" , "@babel/preset-react" ]
}
修改package.json
1
2
3
4
"scripts" : {
"start" : "webpack-dev-server ." ,
"build" : "webpack ."
} ,
useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 官方文档中推荐的做法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 的语法
1
useEffect ( effect , dependencies )
effect
是一个函数,它描述了要执行的副作用操作。它可以返回一个清理函数,用于在组件卸载或下一次effect执行之前清理一些副作用。
dependencies
是一个可选的数组,用于指定 effect 依赖的状态或属性。只有当这些依赖项发生变化时,effect 函数才会被重新执行。如果不提供这个数组,effect 将在每次组件渲染后执行。
useEffect 的执行时机
初次渲染时,effect 函数会被执行一次。
在每次更新后,React 会先比较 effect 的依赖项是否发生变化。如果发生变化,effect 函数就会被重新执行。
如果 effect 函数返回了一个清理函数,那么在下一次effect执行之前或组件卸载之前,这个清理函数会被执行。
注意事项
不要在 effect 函数内部定义函数 。这会导致每次渲染时都创建一个新的函数实例,从而影响依赖项的比较。相反,应该在组件函数作用域内定义并传递给 effect。
注意依赖项的顺序 。依赖项数组中的值顺序发生变化也会导致 effect 重新执行。
避免在 effect 中执行昂贵的操作 。如果需要执行昂贵的操作,可以考虑使用 useMemo
或 useCallback
等钩子进行性能优化。
使用示例
模拟组件生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React , { useEffect } from 'react' ;
function MyComponent () {
useEffect (() => {
// 这里是副作用代码,会在组件渲染后执行
console . log ( '组件已挂载或更新' );
// 清理函数,用于在组件卸载或更新前执行清理工作
return () => {
console . log ( '组件即将卸载或更新,执行清理' );
};
}, []); // 空依赖数组表示这个副作用只在组件挂载时运行一次
return (
< div >
< p > 这里是组件内容 </ p >
</ div >
);
}
export default MyComponent ;
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的第一个回调函数就会被调用,然后再次打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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前
点击后
订阅和取消订阅事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 >
);
}
数据获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
属性不会在渲染时更新。
基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 >
</>
);
}
跨渲染周期保持状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 转发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
接受两个参数:一个函数和一个依赖数组。函数返回的值会被缓存,并且只有当依赖数组中的值发生变化时,函数才会重新执行。
1
2
3
4
5
6
7
8
9
10
11
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
可能是更合适的选择。
示例
缓存复杂计算结果
1
2
3
4
5
6
7
8
9
10
11
12
function CalculationComponent ({ initialData }) {
// 一个复杂的计算函数
const calculateData = () => {
// ...复杂的计算逻辑
return complexResult ;
};
// 使用 useMemo 缓存计算结果
const data = useMemo ( calculateData , [ initialData ]);
return < div > Data : { data }</ div >;
}
在这个例子中,calculateData
函数的结果被缓存,只有当 initialData
发生变化时,计算才会重新执行。
缓存函数
如果你需要缓存一个函数,而不是值,可以使用 useCallback
。但是,如果你的函数返回一个值,并且这个值依赖于函数的参数,你可以使用 useMemo
来缓存这个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
。
useReducer
https://kothing.github.io/react-hook-useReducer/