useMemo
const cachedValue = useMemo(calculateValue, dependencies);
参数一:计算要缓存的值的函数。它应该是纯的,不应该接受任何参数,并且应该返回任何类型的值。React会在初始渲染时调用你的函数。在下一次渲染时,如果依赖关系自上次渲染以来没有改变,React将再次返回相同的值。否则,它将调用calculateValue,返回其结果,并将其存储起来,以便以后重用。
参数二:响应式值包括props、state以及所有直接在组件内部声明的变量和函数。
React将使用Object将每个依赖项与之前的值进行比较。
useMemo是一个钩子,所以你只能在组件的顶层或者你自己的钩子上调用它。你不能称之为内循环或条件。如果需要,提取一个新组件并将状态移到其中。
推荐的使用场景?
使用useMemo进行优化只在少数情况下有价值:
1、在useMemo中进行的计算明显很慢,而且它的依赖项很少改变。
2、您将它作为道具传递给用memo包装的组件。如果值没有改变,您希望跳过重新渲染。Memoization允许组件仅在依赖项不相同时才重新呈现。
3、你传递的值稍后会作为某个Hook的依赖项使用。例如,可能另一个useMemo计算值依赖于它。或者你可能依赖于useEffect的这个值。
在其他情况下,在useMemo中包装计算没有任何好处。这样做也没有什么大的害处,所以有些团队选择不考虑个别案例,而是尽可能多地记忆。这种方法的缺点是代码可读性变差。同样,并不是所有的记忆都是有效的:一个“总是新的”的值就足以破坏整个组件的记忆。
应用场景一(跳过昂贵的计算):
默认情况下,每次重新渲染时,React都会重新运行组件的整个主体。
通常,这不是问题,因为大多数计算都非常快。但是,如果您正在过滤或转换一个大型数组,或者进行一些昂贵的计算,如果数据没有更改,您可能希望跳过再次执行该操作。
如何判断计算是否昂贵?
console.time('filter array');
const visibleTodos = useMemo(() => {
return filterTodos(todos, tab); // Skipped if todos and tab haven't changed
}, [todos, tab]);
console.timeEnd('filter array');
如果总的记录时间加起来很大(比如1毫秒或更多),那么记住这个计算可能是有意义的。作为实验,您可以将计算封装在useMemo中,以验证该交互的总日志时间是否减少了
应用场景二(跳过组件重新渲染):
默认情况下,当一个组件重新呈现时,React会递归地重新呈现它的所有子组件。这就是为什么当TodoList用不同的主题重新呈现时,List组件也会重新呈现的原因。这对于不需要太多计算来重新渲染的组件来说是很好的。但是,如果你已经证实了重新渲染是缓慢的,你可以告诉List跳过重新渲染。
可以使用memo包裹子组件
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
加了 这个之后当props没发生变化的时候,子组件不会重新渲染
export default function TodoList({ todos, tab, theme }) {
// Every time the theme changes, this will be a different array...
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
{/* ... so List's props will never be the same, and it will re-render every time */}
<List items={visibleTodos} />
</div>
);
}
但是父组件传递过来的props 每次在theme变化时都会创建一个新的函数,这种情况下子组件使用的memo并没有起作用。
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...so as long as these dependencies don't change...
);
可以使用useMemo来缓存这个props
问题探讨:为什么不适应useMemo 来包裹一个jsx二是使用memo?
export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
return (
<div className={theme}>
{children}
</div>
);
}
手动将JSX节点包装到useMemo中并不方便。例如,您不能有条件地执行此操作。这就是为什么要用memo包装组件而不是包装JSX节点的原因。
应用场景三(记住另一个钩子的依赖项):
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...
当组件重新呈现时,直接在组件主体内的所有代码将再次运行。创建searchOptions对象的代码行也将在每次重新呈现时运行。因为searchOptions是useMemo调用的依赖项,每次都不一样
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ Only changes when allItems or text changes
// ...
除了将searchOptions 用useMemo 包裹,更优雅的写法是直接定义到hook内部;
应用场景四(记住一个函数):
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
使用useMemo记忆handleSubmit函数
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
也可以使用 useCallback
useCallback
const cachedFn = useCallback(fn, dependencies)
useCallback 和 useMemo 的区别
1、useMemo缓存调用函数的结果。
2、useCallback缓存函数本身。与useMemo不同,它不调用您提供的函数。
使用useCallback缓存函数只在少数情况下有价值:
1、您将它作为道具传递给用memo包装的组件。如果值没有改变,您希望跳过重新渲染。Memoization允许组件仅在依赖项发生变化时才重新呈现。
2、您传递的函数稍后会作为某个Hook的依赖项使用。例如,封装在useCallback中的另一个函数依赖于它,或者您依赖于useEffect中的这个函数。
useContext
在组件的顶层调用 useContext 来读取和订阅 context。
注意事项:
1、useContext 为调用组件返回 context 的值。它被确定为传递给树中调用组件上方最近的 SomeContext.Provider 的 value
2、组件中的 useContext() 调用不受 同一 组件返回的 provider 的影响。相应的 <Context.Provider> 需要位于调用 useContext() 的组件 之上
const ThemeContext = createContext(null);
你可以通过 context 传递任何值,包括对象和函数。
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
const value = useContext(SomeContext)
useEffect
1.1 仅初始化执行,模拟 componentDidMount
依赖空数组,由于依赖为空,不会变化,只执行一次
useEffect(() => {
console.log('hello world')
}, [])
1.2 仅更新执行,模拟 componentDidUpdate
依赖为具体变量,每次变量变化,都执行一次
useEffect(() => {
console.log('info: ', name, age)
}, [name, age])
1.3 初始化和更新都执行,模拟 componentDidMount 和 componentDidUpdate
没有依赖,与依赖为空不同,这个每次都会执行
useEffect(() => {
console.log('every time')
})
1.4 卸载执行,模拟 componentWillUnmount
在useEffect中返回一个函数,则在组件卸载时,会执行改函数
复制代码
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
- 执行async函数
useEffect的回调函数,不能是async,可以将async写在回调里面,单独调用
复制代码
//不可以
useEffect(async()=>{
const res = await fetchData(id)
setData(res.data)
},[id])
//推荐
useEffect(()=>{
const getData = async() => {
const res = await fetchData(id)
setData(res.data)
}
getData()
},[id])
- useEffect执行顺序
useEffect的执行时机,是在react的commit阶段之后。
当父子组件都有useEffect,则先执行子组件的useEffect,再执行父组件的useEffect
使用示例1:
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]);
由于count是一个响应值,因此必须在依赖项列表中指定它。但是,这会导致每次计数更改时都会重新清理和设置Effect。这并不理想。
要解决这个问题,将c => c + 1状态更新程序传递给setCount: setCount(c => c + 1); // ✅ Pass a state updater
使用示例2:
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
如果你的效果依赖于渲染过程中创建的对象或函数,它可能会运行得太频繁。例如,这个效果在每次渲染后都会重新连接,因为每次渲染的选项对象是不同的;
可以将对象或函数定义在 useEffect 内部
useId
useId 是一个新的钩子,用于在客户机和服务器上生成唯一的 id,同时避免水合不匹配。它主要用于组件库与需要唯一 id 的可访问性 api 集成。这解决了一个在 React 17及以下版本中已经存在的问题,但是在 React 18中这个问题更加重要,因为新的媒体伺服器渲染器是如何无序地传递 HTML 的。
主要用于:
为无障碍属性生成唯一 ID
为多个相关元素生成 ID
为所有生成的 ID 指定共享前缀
不能作为列表生成key 使用
useImperativeHandle
useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由 ref 暴露出来的句柄。
useImperativeHandle(ref, createHandle, dependencies?)
ref:该 ref 是你从 forwardRef 渲染函数 中获得的第二个参数。
createHandle:该函数无需参数,它返回你想要暴露的 ref 的句柄。该句柄可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。
可选的 dependencies:函数 createHandle 代码中所用到的所有反应式的值的列表。反应式的值包含 props、状态和其他所有直接在你组件体内声明的变量和函数。倘若你的代码检查器已 为 React 配置好,它会验证每一个反应式的值是否被正确指定为依赖项。该列表的长度必须是一个常数项,并且必须按照 [dep1, dep2, dep3] 的形式罗列各依赖项。React 会使用 Object.is 来比较每一个依赖项与其对应的之前值。如果一次重新渲染导致某些依赖项发生了改变,或你没有提供这个参数列表,你的函数 createHandle 将会被重新执行,而新生成的句柄则会被分配给 ref。
父组件:
import { useRef } from 'react';
import MyInput from './MyInput.js';
export default function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
// 下方代码不起作用,因为 DOM 节点并未被暴露出来:
// ref.current.style.opacity = 0.5;
}
return (
<form>
<MyInput label="Enter your name:" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
子组件
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
警告:
不要滥用 ref。 你应当仅在你没法通过 prop 来表达 命令式 行为的时候才使用 ref:例如,滚动到指定节点、聚焦某个节点、触发一次动画,以及选择文本等等。
如果可以通过 prop 实现,那就不应该使用 ref。例如,你不应该从一个 Model 组件暴露出 {open, close} 这样的命令式句柄,最好是像 这样,将 isOpen 作为一个 prop。副作用 可以帮你通过 prop 来暴露一些命令式的行为。
useLayoutEffect
uselayouteeffect会影响性能。尽可能使用useEffect。
uselayouteeffect是useEffect的一个版本,在浏览器重新绘制屏幕之前触发。
useLayoutEffect(setup, dependencies?)
uselayouteeffect是一个钩子,所以你只能在组件的顶层或者你自己的钩子上调用它。你不能称之为内循环或条件。如果你需要,提取一个组件,并将效果移动到那里。
效果只在客户端上运行。它们不会在服务器渲染期间运行。
uselayouteeffect中的代码和从它调度的所有状态更新阻止浏览器重新绘制屏幕。当过度使用时,这会使你的应用变慢。如果可能的话,使用useEffect。
在浏览器重新绘制屏幕之前测量布局
大多数组件不需要知道它们在屏幕上的位置和大小来决定渲染什么。它们只返回一些JSX。然后浏览器计算它们的布局(位置和大小)并重新绘制屏幕。
有时候,这还不够。假设工具提示出现在悬停时的某个元素旁边。如果有足够的空间,工具提示应该出现在元素的上方,但如果它不合适,它应该出现在元素的下方。为了在正确的最终位置呈现工具提示,你需要知道它的高度(即它是否适合顶部)。
要做到这一点,你需要在两个通道中渲染:
在任何地方渲染工具提示(即使是错误的位置)。
测量其高度并决定放置工具提示的位置。
在正确的位置再次呈现工具提示。
所有这些都需要在浏览器重新绘制屏幕之前完成。您不希望用户看到工具提示在移动。调用uselayouteeffect在浏览器重新绘制屏幕之前执行布局测量
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init?)
参数:
reducer:指定如何更新状态的reducer函数。它必须是纯的,应该将状态和动作作为参数,并且应该返回下一个状态。状态和动作可以是任何类型(这里可以定义一些改变state状态的方法)
initialArg:计算初始状态的值。它可以是任何类型的值。如何从它计算初始状态取决于下一个init参数。
可选的init:应该返回初始状态的初始化函数。如果未指定,则初始状态设置为initialArg。否则,初始状态被设置为调用init(initialArg)的结果。
第三个参数init是干啥用的?
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
如果有上面这种情况,想将接收到的username 经过处理后作为初始值传入,尽管createInitialState(username)的结果仅用于初始呈现,但您仍然在每次呈现时调用该函数。如果要创建大型数组或执行昂贵的计算,这可能会造成浪费。
为了解决这个问题,你可以将它作为初始化函数传递给useReducer作为第三个参数:
const [state, dispatch] = useReducer(reducer, username, createInitialState);
注意,您传递的是createInitialState,这是函数本身,而不是createInitialState(),这是调用它的结果。这样,初始化后就不会重新创建初始状态
返回值:
useReducer返回一个包含两个值的数组:
当前状态。在第一次渲染期间,它被设置为init(initialArg)或initialArg(如果没有init)。
调度函数,允许您将状态更新为不同的值并触发重新呈现。
import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false
});
break;
}
case 'changed': {
const index = draft.findIndex(t =>
t.id === action.task.id
);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useImmerReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false },
];
警告
useReducer是一个钩子,所以你只能在组件的顶层或者你自己的钩子上调用它。你不能称之为内循环或条件。如果需要,提取一个新组件并将状态移到其中。
在严格模式下,React会两次调用你的reducer和初始化器,以帮助你找到意外的杂质。这是仅用于开发的行为,不会影响生产。如果您的减速器和初始化器是纯的(它们应该是纯的),这应该不会影响您的逻辑。其中一个调用的结果将被忽略。
useReducer返回的分派函数允许您将状态更新为不同的值并触发重新呈现。你需要把这个动作作为唯一的参数传递给分派函数:
dispatch 函数:
React将把当前状态和动作传递给reducer函数。您的减速器将计算并返回下一个状态。React将存储下一个状态,用它呈现你的组件,并更新UI。
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
也可以使用switch
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
React将把下一个状态设置为调用reducer函数的结果,该函数提供了当前状态和传递给dispatch的操作。
参数
action:用户执行的操作。它可以是任何类型的值。按照惯例,操作通常是一个带有类型属性标识它的对象,还可以选择带有附加信息的其他属性。
返回
dispatch函数没有返回值。
警告
分派函数仅为下一次呈现更新状态变量。如果在调用分派函数之后读取状态变量,您仍然会得到调用之前屏幕上的旧值。那怎么办?可以调用reducer获取最新值
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
如果您提供的新值与由Object确定的当前状态相同。如果是比较,React将跳过重新渲染组件及其子组件。这是一个优化。React可能仍然需要在忽略结果之前调用你的组件,但它不应该影响你的代码。
React批量状态更新。它在所有事件处理程序运行并调用它们的set函数后更新屏幕。这可以防止在单个事件期间多次重新渲染。在极少数情况下,你需要强制React更新屏幕更早,例如访问DOM,你可以使用flushSync。
不能直接修改对象或者数组;
您改变了一个现有的状态对象并返回了它,因此React忽略了更新。要解决这个问题,您需要确保始终更新状态中的对象和状态中的数组,而不是更改它们
可以引入immer
Immer让你编写简洁的代码,就像你在改变对象一样,但在底层它执行不可变更新
import { useImmerReducer } from 'use-immer';
const [tasks, dispatch] = useImmerReducer(
tasksReducer,
initialTasks
);
你可能会得到一个错误,说:太多的重新渲染。React限制了渲染的次数,以防止无限循环。通常,这意味着您在呈现过程中无条件地分派一个动作,因此您的组件进入了一个循环:呈现、分派(导致呈现)、呈现、分派(导致呈现),等等。通常,这是由指定事件处理程序时的错误引起的
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
对比 useState 和 useReducer
Reducers 并非没有缺点!以下是比较它们的几种方法:
代码体积: 通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
可读性: 当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
可调试性: 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。
可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的!
如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useState 和 useReducer。
useState
const [state, setState] = useState(initialState);
参数:
initialState:初始状态的值。它可以是任何类型的值,但是对于函数有一个特殊的行为。这个参数在初始渲染之后被忽略。
如果将函数作为initialState传递,它将被视为初始化函数。它应该是纯的,不应该接受任何参数,并且应该返回任何类型的值。React将在初始化组件时调用你的初始化函数,并将其返回值存储为初始状态。请看下面的例子。
返回:
useState返回一个包含两个值的数组;
初始化的时候传递一个函数行不行?
const [todos, setTodos] = useState(createInitialTodos());
尽管createInitialTodos()的结果仅用于初始呈现,但您仍然在每次呈现时调用该函数。如果要创建大型数组或执行昂贵的计算,这可能会造成浪费。
为了解决这个问题,你可以把它作为一个初始化函数传递给useState:
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
useState是一个钩子,所以你只能在组件或你自己的钩子的顶层调用它。你不能称之为内循环或条件。如果需要,提取一个新组件并将状态移到其中。
在严格模式下,React将调用你的初始化函数两次,以帮助你找到意外的杂质。这是仅用于开发的行为,不会影响生产。如果你的初始化函数是纯函数(应该是纯函数),这应该不会影响行为。其中一个调用的结果将被忽略。
useState返回一个包含两个值的数组:
当前状态。在第一次渲染期间,它将匹配您传递的initialState。
set函数允许您将状态更新为不同的值并触发重新呈现:
useState返回的set函数允许您将状态更新为不同的值并触发重新呈现。你可以直接传递下一个状态,或者一个从前一个状态计算它的函数;
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
怎么获得最新state的值?
set函数仅为下一次呈现更新状态变量。如果在调用set函数之后读取状态变量,您仍然会得到调用之前屏幕上的旧值。
React批量状态更新。它在所有事件处理程序运行并调用它们的set函数后更新屏幕。这可以防止在单个事件期间多次重新渲染。在极少数情况下,你需要强制React更新屏幕更早,例如访问DOM,你可以使用flushSync。
const nextCount = count + 1; 自己定义一个变量
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
连续调用了两次set方法,state数据值改变了一次?
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1) 下面两次的age还是42
setAge(age + 1); // setAge(42 + 1)
}
为了解决这个问题,你可以传递一个update函数给setAge而不是next状态
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
改变再state中定义的函数和数组,没有变化?
您可以将对象和数组置于状态。在React中,状态被认为是只读的,所以你应该替换它,而不是改变你现有的对象。例如,如果你有一个处于状态的form对象,不要改变它;
改变对象
setForm({
...form,
firstName: 'Taylor'
});
改变深层次的对象
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
改变数组
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(initialTodos);
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
}
function handleChangeTodo(nextTodo) {
setTodos(todos.map(t => {
if (t.id === nextTodo.id) {
return nextTodo;
} else {
return t;
}
}));
}
function handleDeleteTodo(todoId) {
setTodos(
todos.filter(t => t.id !== todoId)
);
}
也可以使用immer
import { useImmer } from 'use-immer';
通过传入key来重置state的状态?
你可以通过给组件传递一个不同的键来重置组件的状态。在这个例子中,Reset按钮改变了版本状态变量,我们将它作为键传递给Form。当键改变时,React会从头开始重新创建Form组件(以及它的所有子组件),所以它的状态会被重置。
export default function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
function Form() {
const [name, setName] = useState('Taylor');
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Hello, {name}.</p>
</>
);
}
想要拿到上一次的props对比后操作?
可以使用state来实现
const [prevCount, setPrevCount] = useState(count);
const [trend, setTrend] = useState(null);
if (prevCount !== count) {
setPrevCount(count);
setTrend(count > prevCount ? 'increasing' : 'decreasing');
}
I’m getting an error: “Too many re-renders” ?
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
如果想再state里存一个函数怎么写?
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}
useRef
const ref = useRef(initialValue)
useRef 是一个 React Hook,它能让你引用一个不需要渲染的值。
参数 :
initialValue:ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数会首次渲染后被忽略。
返回值 :
useRef 返回一个只有一个属性的对象:
current:最初,它被设置为你传递的 initialValue。之后你可以把它设置为其他值。如果你把 ref 对象作为一个 JSX 节点的 ref 属性传递给 React,React 将为它设置 current 属性。
在后续的渲染中,useRef 将返回同一个对象。
注意事项
1、你可以修改 ref.current 属性。与 state 不同,它是可变的。然而,如果它持有一个用于渲染的对象(例如,你的 state 的一部分),那么你就不应该修改这个对象。
2、当你改变 ref.current 属性时,React 不会重新渲染你的组件。React 不知道你何时改变它,因为 ref 是一个普通的 JavaScript 对象。
3、除了 初始化 外不要在渲染期间写入 或者读取 ref.current。这会使你的组件的行为不可预测。
在严格模式下,React 将会 调用两次组件方法,这是为了 帮助你发现意外的问题。这只是开发模式下的行为,不影响生产模式。每个 ref 对象将会创建两次,但是其中一个版本将被丢弃。如果你的组件函数是纯的(应该如此),这不会影响其行为。
4、不要在渲染期间写入 或者读取 ref.current。
function MyComponent() {
// ...
// 🚩 不要在渲染期间写入 ref
myRef.current = 123;
// ...
// 🚩 不要在渲染期间读取 ref
return <h1>{myOtherRef.current}</h1>;
}
你可以在 事件处理程序或者 effects 中读取和写入 ref。
function MyComponent() {
// ...
useEffect(() => {
// ✅ 你可以在 effects 中读取和写入 ref
myRef.current = 123;
});
// ...
function handleClick() {
// ✅ 你可以在事件处理程序中读取和写入 ref
doSomething(myOtherRef.current);
}
// ...
}
使用方法
方式一 : 用 ref 引用一个值
function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000);
intervalRef.current = intervalId;
}
通过使用 ref,你可以确保:
你可以在重新渲染之间 存储信息(不像是普通对象,每次渲染都会重置)。
改变它 不会触发重新渲染(不像是 state 变量,会触发重新渲染)。
对于你的组件的每个副本来说,这些信息都是本地的(不像是外面的变量,是共享的)。
改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息。如有需要,使用 state 代替
方式二、通过 ref 操作 DOM
当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为你的 ref 对象的 current 属性。现在你可以访问 的 DOM 节点,并且可以调用类似于 focus() 的方法:
const inputRef = useRef(null);
return <input ref={inputRef} />;
function handleClick() {
inputRef.current.focus();
}
子组件向父组件暴露ref?
const MyInput = forwardRef((props, ref) => { 子组件使用forwardRef 包裹
return <input {...props} ref={ref} />;
});
<MyInput ref={inputRef} /> 父组件传一个ref给子组件
function handleClick() {
inputRef.current.focus();
}
useRef和sueState的区别,如何选择?
ref.current 属性访问该 ref 的当前值。这个值是有意被设置为可变的,意味着你既可以读取它也可以写入它。就像一个 React 追踪不到的、用来存储组件信息的秘密“口袋”。(这就是让它成为 React 单向数据流的“应急方案”的原因
何时使用 ref
通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:
存储 timeout ID
存储和操作 DOM 元素,我们将在 下一页 中介绍
存储不需要被用来计算 JSX 的其他对象。
如果你的组件需要存储一些值,但不影响渲染逻辑,请选择 ref。
ref 的最佳实践
遵循这些原则将使你的组件更具可预测性:
1、将 ref 视为应急方案。 当你使用外部系统或浏览器 API 时,ref 很有用。如果你很大一部分应用程序逻辑和数据流都依赖于 ref,你可能需要重新考虑你的方法。
2、不要在渲染过程中读取或写入 ref.current。 如果渲染过程中需要某些信息,请使用 state 代替。由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)
3、React state 的限制不适用于 ref。例如,state 就像 每次渲染的快照,并且 不会同步更新。但是当你改变 ref 的 current 值时,它会立即改变:
ref.current = 5;
console.log(ref.current); // 5
这是因为 ref 本身是一个普通的 JavaScript 对象, 所以它的行为就像对象那样。
当你使用 ref 时,也无需担心 避免变更。只要你改变的对象不用于渲染,React 就不会关心你对 ref 或其内容做了什么。
useTransition()
useTransition 是一个让你在不阻塞 UI 的情况下来更新状态的 React Hook。
用来不阻塞的更新,传递的是改变状态的hook
const [isPending, startTransition] = useTransition()
参数
useTransition 不需要任何参数。
返回值
useTransition 返回一个由两个元素组成的数组:
isPending 标志,告诉你是否存在待处理的转换。
startTransition 函数 允许你将状态更新标记为转换状态。
startTransition 函数 参数
作用域(scope):一个通过调用一个或多个 set 函数。 函数更新某些状态的函数。React 会立即不带参数地调用此函数,并将在作用域函数调用期间计划同步执行的所有状态更新标记为转换状态。它们将是非阻塞的,并且 不会显示不想要的加载指示器。
startTransition 函数 返回值
startTransition 不会返回任何值。
用法:
1、将状态更新标记为非阻塞转换状态
2、在转换中更新父组件
3、在转换期间显示待处理的视觉状态