useState
状态管理
useState
是 React 中的一个基础 Hook,允许你在不使用 class 组件的情况下管理组件状态。
参数
初始值
你可以直接传递状态的初始值给 useState
:
js
const [name, setName] = useState("John");
使用函数设置初始值
当初始化状态代价较大时,你可以传递一个函数:
js
const [state, setState] = useState(() => {
const initialState = calculateInitialState(); // 一些复杂的操作
return initialState;
});
返回值
useState
返回一个数组,其中包括当前状态值和一个更新状态的函数。
示例
基础计数器示例
js
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
使用对象作为状态
js
function UserProfile() {
const [profile, setProfile] = useState({ name: "John", age: 30 });
const updateName = name => {
setProfile(prevProfile => ({ ...prevProfile, name }));
};
return (
<div>
<p>Name: {profile.name}</p>
<p>Age: {profile.age}</p>
<button onClick={() => updateName("Doe")}>Update Name</button>
</div>
);
}
使用多个 useState
你可以在一个组件中使用多个 useState
来管理不同的状态片段:
js
function MultiCounter() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div>
<p>Counter 1: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>Increment Counter 1</button>
<p>Counter 2: {count2}</p>
<button onClick={() => setCount2(count2 + 1)}>Increment Counter 2</button>
</div>
);
}
注意事项
-
调用位置 :在 React 函数的最顶层调用
useState
。 -
依赖之前的状态更新 :使用函数式更新,如:
setCount(prevCount => prevCount + 1)
。 -
不会自动合并对象 :手动合并对象,如:
setState(prevState => ({ ...prevState, key: 'value' }))
。
useEffect
副作用操作
React 函数组件的副作用利器
什么是副作用?
在编程中,副作用是指代码对外部世界产生的影响。比如说,你想在用户点击按钮后改变网页的标题。这个改变就是一个副作用。
为什么需要 useEffect
?
React 组件通常用于渲染 UI,但有时你还需要执行一些额外的操作,如网络请求、操作 DOM 或订阅事件等。这些操作就是所谓的副作用,而 useEffect
就是用来处理这些副作用的工具。
如何使用 useEffect
?
useEffect
的使用很简单。它接受两个参数:一个副作用函数和一个依赖数组。
- 副作用函数:放置你想执行的副作用代码。
- 依赖数组:决定副作用何时执行。数组中的值发生变化时,副作用会重新执行。
例子
改变网页标题
假设你想在组件挂载后改变网页标题。你可以这样做:
js
useEffect(() => {
document.title = "新的标题";
}, []); // 空数组表示只在组件挂载后执行一次
获取用户数据
如果你想根据用户 ID 获取用户数据,你可以这样做:
js
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/user/${userId}`)
.then(response => response.json())
.then(setUser);
}, [userId]); // 当 userId 改变时,重新获取数据
}
注意事项
-
清理副作用:如果你的副作用涉及需要清理的操作(例如取消订阅事件),你可以在副作用函数中返回一个清理函数。
jsuseEffect(() => { const timerId = setInterval(() => { console.log("Tick"); }, 1000); return () => { clearInterval(timerId); // 清理定时器 }; }, []);
-
避免无限循环 :不小心使用
useEffect
可能导致无限循环。务必注意你的依赖数组,确保副作用按预期执行。
useMemo
记忆计算值
useMemo
钩子用于在组件渲染期间记住复杂计算的结果。这可以提高性能,尤其是当有大量计算和重新计算时。
为什么使用 useMemo
?
当组件重新渲染时,可能会涉及到一些复杂的计算。如果这些计算的依赖项没有改变,那么重新进行这些计算就是浪费。useMemo
允许你记住这些计算的结果,只有当依赖项改变时才重新计算。
如何使用 useMemo
?
useMemo
接受两个参数:一个函数和一个依赖项数组。它会返回该函数的返回值。
- 函数:包含要记住的计算。
- 依赖项数组:当数组中的任何值发生变化时,函数将被重新执行。
TypeScript 示例代码
下面的示例展示了如何使用 useMemo
来过滤大于 10 的数字。只有当数字数组改变时,才会重新计算过滤的结果。
ts
import React, { useMemo, useState } from "react";
interface Props {
items: number[];
}
const ExpensiveComponent: React.FC<Props> = ({ items }) => {
const filteredItems = useMemo(() => items.filter(item => item > 10), [items]);
return (
<div>
<h3>Filtered Items (Greater than 10):</h3>
{filteredItems.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
};
const App: React.FC = () => {
const [items, setItems] = useState<number[]>([5, 12, 8, 20, 33]);
const addItem = () => setItems([...items, Math.floor(Math.random() * 40)]);
return (
<div>
<button onClick={addItem}>Add Item</button>
<h3>All Items:</h3>
{items.map(item => (
<div key={item}>{item}</div>
))}
<ExpensiveComponent items={items} />
</div>
);
};
export default App;
注意事项
- 不要将
useMemo
作为性能优化的首要工具。除非你确实遇到性能问题,并且确定了useMemo
可以解决问题,否则不要过早优化。 - 不应该在
useMemo
的函数内部执行有副作用的操作。使用useEffect
处理副作用。
总结
useMemo
是一个非常强大的工具,可以提高组件的性能,尤其是在复杂的计算和频繁的重新渲染中。但是,也要谨慎使用它,确保只在确实需要时使用它,并始终确保你的代码的正确性和可维护性。
useCallback
:记忆化函数
什么是 useCallback
?
useCallback
用于在组件的连续渲染之间记忆化(或缓存)函数。它帮助避免因父组件重新渲染而导致的不必要的子组件渲染。
如何使用?
useCallback
接受两个参数:一个函数和一个依赖数组。只有当依赖数组中的值发生变化时,函数才会被重新创建。
示例
让我们看一个完整的示例,展示如何使用 useCallback
来优化组件的渲染。
js
import React, { useState, useCallback } from "react";
const ChildComponent = React.memo(({ onClick }) => {
console.log("ChildComponent re-rendered!");
return <button onClick={onClick}>Click Me!</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
// 使用 useCallback 记忆化 handleClick 函数
const handleClick = useCallback(() => {
console.log("Button was clicked");
}, []); // 空依赖数组,表示该函数不会重新创建
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Counter</button>
{/* 传递记忆化的 handleClick 到 ChildComponent */}
<ChildComponent onClick={handleClick} />
</div>
);
}
export default ParentComponent;
在上面的示例中,ChildComponent
接受一个 onClick
属性,并且使用 React.memo
进行了优化,所以只有当 onClick
属性发生变化时才会重新渲染。由于 handleClick
使用 useCallback
被记忆化了,因此即使父组件重新渲染,handleClick
函数也不会更改,从而避免了子组件的不必要渲染。
注意事项
- 过度优化的风险 :不是所有的函数都需要使用
useCallback
。如果一个函数没有传递给子组件或其他记忆化操作,使用useCallback
可能会带来更多的复杂性和性能开销。 - 依赖项的准确性:确保依赖数组中的所有值都是必需的,并且当它们发生变化时,你确实希望函数被重新创建。
总结
useCallback
是一个强大的优化工具,可以在适当的情况下提高 React 组件的性能。通过使用此 Hook,你可以控制函数的重新创建,从而避免因父组件重新渲染而导致的不必要的子组件渲染。
该示例展示了如何在实际组件中使用 useCallback
进行优化,希望这有助于你更深入地理解这个 Hook 的用途和工作方式。如果你有任何问题或需要进一步的解释,请随时提问!
useCallback 和 useMemo 区别
- 用途 :
useCallback
用于记忆化函数,而useMemo
用于记忆化计算值。 - 返回值 :
useCallback
返回一个记忆化的函数;useMemo
返回一个记忆化的计算结果。 - 最佳实践
- 当你需要传递给子组件的函数并希望避免不必要的子组件渲染时,使用
useCallback
。 - 当你有昂贵的计算,并希望在依赖项未更改时避免重新计算时,使用
useMemo
- 当你需要传递给子组件的函数并希望避免不必要的子组件渲染时,使用
useRef
访问 Ref
用于访问和操作 DOM 元素及保持可变的引用值
什么是 useRef
?
useRef
Hook 用于访问和操作 DOM 元素,并在组件的整个生命周期中保持不变的引用。除了用于直接操作 DOM,useRef
还可用于在组件的不同渲染之间保留可变的引用值,而不触发重新渲染。
如何使用 useRef
?
要使用 useRef
,你首先需要调用它,并将初始值作为参数传递(如果有)。这将返回一个 ref
对象,该对象具有一个名为 current
的属性,该属性将引用传递给 useRef
的初始值或 DOM 元素。
使用 useRef
访问 DOM 元素
你可以使用 useRef
直接访问和操作 DOM 元素。下面是一个示例,显示了如何使用 useRef
控制输入元素的焦点。
js
import React, { useRef } from "react";
function TextInput() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // 使用 ref 对象的 current 属性访问输入元素
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>Focus the input</button>
</div>
);
}
在上述示例中,我们创建了一个 ref
对象并将其分配给输入元素。然后,我们可以使用该引用在按钮点击时将焦点设置到输入元素上。
使用 useRef
保留可变的引用值
除了用于访问 DOM 元素外,useRef
还可以用于在组件的连续渲染之间保留可变的引用值,而不触发重新渲染。
js
import React, { useRef, useEffect } from "react";
function Timer() {
const countRef = useRef(0);
useEffect(() => {
const intervalId = setInterval(() => {
countRef.current += 1; // 更新 ref 的 current 值
console.log("Timer:", countRef.current);
}, 1000);
return () => clearInterval(intervalId); // 清除计时器
}, []); // 空依赖数组表示该效果只在挂载和卸载时运行
return <div>Check the console to see the timer count!</div>;
}
在这个示例中,我们使用 useRef
来保持计时器的计数值。因为 ref
对象的更改不会触发组件的重新渲染,所以它是一个非常有用的工具,用于在重新渲染之间保留值。
总结
useRef
是一个多功能的 React Hook,可以用于多种用途:
- 访问和操作 DOM 元素 :使用 ref 对象的
current
属性直接访问和操作 DOM 元素。 - 保留可变引用值:在组件的不同渲染之间保留值,而不触发重新渲染。
自定义组件使用 Ref
在 React 中,你可以使用引用(ref)与自定义组件进行交互,访问组件的实例。这是一个非常有用的特性,可以用于获取组件内部的信息,调用组件内部的方法,或者与组件进行更复杂的交互。
使用 forwardRef
与自定义组件
对于自定义组件,你通常需要使用 forwardRef
来转发 ref。下面是一个简单的示例,显示如何使用 forwardRef
创建自定义组件,并从父组件中访问该组件的 DOM 元素:
js
import React, { useRef, forwardRef } from "react";
const CustomComponent = forwardRef((props, ref) => {
return <div ref={ref}>Custom Component</div>;
});
function App() {
const customComponentRef = useRef(null);
const handleClick = () => {
customComponentRef.current.style.backgroundColor = "lightblue";
};
return (
<div>
<CustomComponent ref={customComponentRef} />
<button onClick={handleClick}>Change Background Color</button>
</div>
);
}
export default App;
使用 useImperativeHandle
暴露自定义实例属性
有时,你可能希望自定义组件能够暴露特定的实例方法或属性给父组件。在这种情况下,你可以使用 useImperativeHandle
进行更精细的控制。下面的示例显示了如何使用 useImperativeHandle
暴露组件的特定方法:
js
import React, { useRef, forwardRef, useImperativeHandle } from "react";
const CustomComponent = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
sayHello: () => {
alert("Hello from CustomComponent!");
},
}));
return <div>Custom Component</div>;
});
function App() {
const customComponentRef = useRef(null);
const handleClick = () => {
customComponentRef.current.sayHello(); // 调用自定义组件的方法
};
return (
<div>
<CustomComponent ref={customComponentRef} />
<button onClick={handleClick}>Say Hello</button>
</div>
);
}
export default App;
总结
与自定义组件一起使用 ref 可以实现许多强大的功能,从访问组件内部的 DOM 元素,到调用组件的特定实例方法。通过使用 forwardRef
和 useImperativeHandle
,你可以更灵活地控制自定义组件的行为,并与之进行更复杂的交互。
请注意,直接操作 DOM 和组件实例通常应该作为最后的手段,因为它可能会导致代码难以理解和维护。在可能的情况下,尽量使用正常的 React 数据流来管理组件的状态和行为。
useContext
访问上下文
useContext
钩子是 React 中一个非常强大的工具,用于在组件树中跨层级共享状态。它消除了通过多层组件手动传递 props 的需要,使得状态管理变得更加清晰和高效。
为什么使用 useContext
?
在复杂的 React 应用程序中,状态需要在多个组件之间共享。传统方法是将状态作为 props 从顶层组件一层一层传递下去,但这会导致代码混乱且难以维护。useContext
允许我们跨越组件层级直接共享状态,无需手动传递。
如何使用 useContext
?
-
创建上下文 : 使用 React 的
createContext
方法创建一个上下文。 -
提供上下文 : 使用
Context.Provider
组件在组件树中提供上下文的值。 -
消费上下文 : 在任何子组件中使用
useContext
钩子访问上下文的值。
TypeScript 示例代码
下面的示例展示了如何使用 useContext
来共享主题设置。
ts
import React, { useContext, useState } from "react";
// 创建上下文
const ThemeContext = React.createContext<{ theme: string; toggleTheme: () => void } | undefined>(undefined);
const App: React.FC = () => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
// 提供上下文
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Header />
<MainContent />
</ThemeContext.Provider>
);
};
const Header: React.FC = () => {
// 消费上下文
const themeContext = useContext(ThemeContext);
if (!themeContext) {
return <h1>Error: Theme not found</h1>;
}
return (
<div>
<h1>{themeContext.theme} theme is active</h1>
<button onClick={themeContext.toggleTheme}>Toggle Theme</button>
</div>
);
};
const MainContent: React.FC = () => {
// ...
return <div>Main Content</div>;
};
export default App;
注意事项
- 确保在使用
useContext
之前已经在组件树中提供了上下文。 - 不要在渲染过程中更改上下文的值,这可能会导致组件的不必要重新渲染。
总结
useContext
钩子是一个强大的工具,用于组件之间的跨层级状态共享。通过消除手动传递 props 的需要,它可以使你的代码更加清晰、简洁,同时也提高了代码的可维护性。
useReducer
复杂状态逻辑
useReducer
是 React 中用于处理组件状态逻辑的钩子,尤其适用于更复杂或包括多个子值的状态。它的工作原理类似于 Redux,但是更加精简,不需要引入额外的库。
为什么使用 useReducer
?
当状态逻辑复杂或者下一状态依赖于之前的状态时,useReducer
非常有用。相较于 useState
,它提供了更可预测的状态更新方式,并且更容易测试和维护。
如何使用 useReducer
?
useReducer
接收两个参数:一个 reducer 函数和初始状态,返回当前状态和一个 dispatch 函数。
-
Reducer 函数: 这个函数接收两个参数:当前的状态和一个动作。根据动作类型,它返回一个新的状态。
-
初始状态: 初始状态是 reducer 的第一个参数的初始值。
-
Dispatch 函数: 这个函数用于分派动作,触发 reducer 函数,并更新状态。
TypeScript 示例代码
下面的示例演示了如何使用 useReducer
来管理一个计数器的状态。
ts
import React, { useReducer } from "react";
// 定义动作类型
type Action = { type: "increment" } | { type: "decrement" };
// 定义reducer函数
const counterReducer = (state: number, action: Action) => {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
return state;
}
};
const Counter: React.FC = () => {
// 使用useReducer
const [state, dispatch] = useReducer(counterReducer, 0);
return (
<div>
<h1>Count: {state}</h1>
<button onClick={() => dispatch({ type: "increment" })}>Increment</button>
<button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
</div>
);
};
export default Counter;
注意事项
- Reducer 必须是一个纯函数,这意味着同样的输入必须产生相同的输出,不应有副作用。
- 尽量使 reducer 函数保持简洁明了,可以通过拆分成多个子 reducer 来处理更复杂的状态逻辑。
总结
useReducer
钩子提供了一种更灵活、可维护的方式来处理复杂的状态逻辑。通过结合纯净的 reducer 函数和动作分派,它为开发人员提供了对组件内部状态更细粒度的控制。对于需要管理复杂状态或者希望与 Redux 保持一致的项目,useReducer
是一个极好的选择。
useImperativeHandle
自定义 ref 暴露
useImperativeHandle
是一种特殊的钩子,允许你在父组件中直接操作子组件中的某些实例方法。通常来说,在 React 中,组件应该遵循数据自上而下的流动,并通过属性和状态进行通信。但有时,你可能需要在父组件中直接调用子组件的某些方法。这就是 useImperativeHandle
发挥作用的地方。
这个钩子不常用,因为它打破了 React 的一些核心原则,但在某些特定场景下可能很有用。
参数
useImperativeHandle
接受三个参数:
- ref: 父组件传递给子组件的 ref 对象。
- createHandle: 一个返回包含暴露给父组件的实例方法的对象的函数。
- deps : 一个依赖数组,类似于
useEffect
或useMemo
等钩子。如果提供了此参数,则只有当依赖项更改时,才会更新实例方法。
示例
假设你有一个 TextInput
组件,并且你想暴露一个方法来清除输入。
ts
import React, { useImperativeHandle, forwardRef, useRef } from "react";
type TextInputHandles = {
clear: () => void;
};
const TextInput = forwardRef<TextInputHandles, {}>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
clear: () => {
if (inputRef.current) {
inputRef.current.value = "";
}
},
}));
return <input type="text" ref={inputRef} />;
});
const ParentComponent = () => {
const inputRef = useRef<TextInputHandles>(null);
const clearInput = () => {
if (inputRef.current) {
inputRef.current.clear();
}
};
return (
<div>
<TextInput ref={inputRef} />
<button onClick={clearInput}>Clear Input</button>
</div>
);
};
export default ParentComponent;
注意事项
- 谨慎使用 : 尽量不要过度使用
useImperativeHandle
,因为它可能导致代码更难理解和维护。 - 与
forwardRef
配合使用 : 通常,你需要将子组件与forwardRef
一起使用,以将 ref 传递给子组件。
总结
虽然 useImperativeHandle
不是常用的钩子,但在需要在父组件中直接操作子组件的特定场景下,它可能是必要的。通过允许你精确地控制父组件可以访问的子组件方法,它提供了一种强大但易于滥用的工具。在使用它时要小心,并确保这确实是解决问题的最佳方式。