当前位置: 首页 > news >正文

React 14

state:组件的记忆案例练习

1 修复画廊翻页BUG代码详细解释

import { useState } from 'react';
import { sculptureList } from './data.js';export default function Gallery() {const [index, setIndex] = useState(0);const [showMore, setShowMore] = useState(false);function handleNextClick() {setIndex(index + 1);}function handleMoreClick() {setShowMore(!showMore);}let sculpture = sculptureList[index];return (<><button onClick={handleNextClick}>Next</button><h2><i>{sculpture.name} </i> by {sculpture.artist}</h2><h3>  ({index + 1} of {sculptureList.length})</h3><button onClick={handleMoreClick}>{showMore ? 'Hide' : 'Show'} details</button>{showMore && <p>{sculpture.description}</p>}<img src={sculpture.url} alt={sculpture.alt}/></>);
}

改后

import { useState } from 'react';
import { sculptureList } from './data.js';export default function Gallery() {const [index, setIndex] = useState(0);const [showMore, setShowMore] = useState(false);let hasPrev = index > 0;let hasNext = index < sculptureList.length - 1;function handlePrevClick() {if (hasPrev) {setIndex(index - 1);}}function handleNextClick() {if (hasNext) {setIndex(index + 1);}}function handleMoreClick() {setShowMore(!showMore);}let sculpture = sculptureList[index];return (<><buttononClick={handlePrevClick}disabled={!hasPrev}>Previous</button><buttononClick={handleNextClick}disabled={!hasNext}>Next</button><h2><i>{sculpture.name} </i> by {sculpture.artist}</h2><h3>  ({index + 1} of {sculptureList.length})</h3><button onClick={handleMoreClick}>{showMore ? 'Hide' : 'Show'} details</button>{showMore && <p>{sculpture.description}</p>}<img src={sculpture.url} alt={sculpture.alt}/></>);
}

我们来详细分析这个代码的变化,理解它修复了什么问题以及为什么这样修复。

先看第一个版本的问题

第一个版本的代码中,只有一个Next按钮,它的点击事件处理函数是这样的:

function handleNextClick() {setIndex(index + 1);
}

这个函数的逻辑很简单:每次点击就把index(当前显示的雕塑索引)加 1。

这里隐藏的 bug 是:当浏览到最后一个雕塑时,继续点击Next按钮会导致错误。

  • 假设sculptureList有 5 个元素(索引 0-4),当index已经是 4(最后一个)时,点击Next会让index变成 5
  • 此时执行let sculpture = sculptureList[index]时,sculptureList[5]是不存在的(数组越界)
  • 后续访问sculpture.namesculpture.url等属性时,就会报错(无法访问undefined的属性)

修复方案:添加边界判断

第二个版本的代码主要做了 3 件事来修复这个问题:

  1. 增加了 “上一个” 按钮和对应的逻辑不仅能往后翻,还能往前翻,提升了用户体验。

  2. 添加了边界状态判断定义了两个变量来判断当前是否能翻页:

    let hasPrev = index > 0; // 是否有上一个(索引大于0时可以往前翻)
    let hasNext = index < sculptureList.length - 1; // 是否有下一个(索引小于最后一个索引时可以往后翻)
    
    • 比如数组长度为 5,最后一个元素的索引是 4,所以index < 4时才有下一个
  3. 在翻页函数中添加判断,并且禁用无效按钮

    • 翻页函数中先判断是否能翻页,只有能翻页时才修改index
      function handlePrevClick() {if (hasPrev) { // 只有有上一个时,才执行翻页setIndex(index - 1);}
      }function handleNextClick() {if (hasNext) { // 只有有下一个时,才执行翻页setIndex(index + 1);}
      }
      
    • 按钮添加disabled属性,当不能翻页时按钮变灰且无法点击:
      <buttononClick={handlePrevClick}disabled={!hasPrev} // 没有上一个时禁用按钮
      >Previous
      </button>
      

总结修复的核心

  1. 防止数组越界:通过hasPrevhasNext判断边界,避免index超出数组的合法索引范围(0 到 数组长度 - 1)。
  2. 优化用户体验:禁用无效状态的按钮,让用户直观知道当前不能继续翻页,避免误操作。

这样修改后,无论怎么点击按钮,index都会保持在合法范围内,也就不会出现访问不存在的数组元素的错误了。

2 修复表单输入

export default function Form() {let firstName = '';let lastName = '';function handleFirstNameChange(e) {firstName = e.target.value;}function handleLastNameChange(e) {lastName = e.target.value;}function handleReset() {firstName = '';lastName = '';}return (<form onSubmit={e => e.preventDefault()}><inputplaceholder="First name"value={firstName}onChange={handleFirstNameChange}/><inputplaceholder="Last name"value={lastName}onChange={handleLastNameChange}/><h1>Hi, {firstName} {lastName}</h1><button onClick={handleReset}>Reset</button></form>);
}

改后

import { useState } from 'react';export default function Form() {const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');function handleFirstNameChange(e) {setFirstName(e.target.value);}function handleLastNameChange(e) {setLastName(e.target.value);}function handleReset() {setFirstName('');setLastName('');}return (<form onSubmit={e => e.preventDefault()}><inputplaceholder="First name"value={firstName}onChange={handleFirstNameChange}/><inputplaceholder="Last name"value={lastName}onChange={handleLastNameChange}/><h1>Hi, {firstName} {lastName}</h1><button onClick={handleReset}>Reset</button></form>);
}

第一个版本的核心问题:普通变量无法触发组件重新渲染

第一个版本中,使用了普通的let变量来存储表单输入值:

let firstName = '';
let lastName = '';

当用户在输入框中输入内容时,handleFirstNameChangehandleLastNameChange确实会修改这两个变量的值。但React 并不知道这些变量的变化,所以不会重新渲染组件。

具体表现为:

  1. 输入框中输入内容时,输入框会 “卡住”,无法显示用户输入的内容(因为value={firstName}绑定的是不会被 React 追踪的普通变量)。
  2. 页面上的<h1>Hi, {firstName} {lastName}</h1>也不会随着输入更新,始终显示Hi, 
  3. 点击 “Reset” 按钮时,虽然变量被重置为'',但页面同样不会有任何变化。

这是因为React 组件的重新渲染只依赖于 “状态(state)” 或 “属性(props)” 的变化,普通变量的修改不会触发重新渲染。

第二个版本的修复:使用 React 状态(useState)

第二个版本引入了useState钩子来管理表单状态,这是修复问题的核心:

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

useState是 React 提供的用于管理组件内部状态的钩子,它的工作原理和作用如下:

  1. 状态变量(如 firstName):由useState创建的变量,React 会追踪它的变化。
  2. 状态更新函数(如 setFirstName):用于修改状态变量的值。关键是:调用这个函数后,React 会知道状态发生了变化,并自动重新渲染组件

修复后的具体变化分析

  1. 输入框与状态的双向绑定

    • 输入框的value绑定到状态变量(value={firstName}),确保输入框显示的是当前状态值。
    • 当用户输入时,onChange触发handleFirstNameChange,通过setFirstName(e.target.value)更新状态。
    • 状态更新后,React 重新渲染组件,输入框会显示新的状态值,实现了 “输入即显示” 的效果。
  2. 页面内容实时更新

    • 由于<h1>Hi, {firstName} {lastName}</h1>依赖状态变量,当状态更新并重新渲染时,这里的内容会自动更新为最新的输入值。
  3. Reset 按钮生效

    • 点击 “Reset” 时,handleReset通过setFirstName('')setLastName('')将状态重置为空。
    • 状态变化触发重新渲染,输入框和<h1>都会显示为空,实现了重置效果。

总结:核心修复逻辑

第一个版本的问题在于使用普通变量存储状态,变量变化无法被 React 感知,导致组件不重新渲染,页面无法更新。

第二个版本通过useState 钩子将普通变量转换为 React 状态,状态的每次更新(通过setXXX函数)都会触发组件重新渲染,从而让输入框、页面内容等依赖状态的部分实时更新,最终实现了表单的正常交互功能。

这也是 React 中 “状态驱动视图” 理念的体现:视图(UI)的变化应该由状态的变化来驱动,而状态必须通过 React 提供的机制(如useState)来管理,才能被 React 追踪和响应。

3 弹窗错误

import { useState } from 'react';export default function FeedbackForm() {const [isSent, setIsSent] = useState(false);if (isSent) {return <h1>Thank you!</h1>;} else {// eslint-disable-next-lineconst [message, setMessage] = useState('');return (<form onSubmit={e => {e.preventDefault();alert(`Sending: "${message}"`);setIsSent(true);}}><textareaplaceholder="Message"value={message}onChange={e => setMessage(e.target.value)}/><br /><button type="submit">Send</button></form>);}
}

改后

import { useState } from 'react';export default function FeedbackForm() {const [isSent, setIsSent] = useState(false);const [message, setMessage] = useState('');if (isSent) {return <h1>Thank you!</h1>;} else {return (<form onSubmit={e => {e.preventDefault();alert(`Sending: "${message}"`);setIsSent(true);}}><textareaplaceholder="Message"value={message}onChange={e => setMessage(e.target.value)}/><br /><button type="submit">Send</button></form>);}
}

我们来详细分析这两段代码的区别,以及为什么第二个版本修复了第一个版本的问题。

第一个版本的核心问题:条件渲染中声明状态(useState)

第一个版本的代码结构是这样的:

const [isSent, setIsSent] = useState(false);
if (isSent) {return <h1>Thank you!</h1>;
} else {// 在 else 分支中声明了另一个状态const [message, setMessage] = useState('');// ... 返回表单
}

这里的关键问题是:message状态被声明在了else条件分支内部

React 对状态钩子(useState)有一个严格的规则:所有的useState调用必须在组件的顶层作用域(即不能在条件、循环、嵌套函数中)

为什么会有这个规则?React 是通过状态声明的顺序来追踪每个状态的(而不是变量名)。如果在条件分支中声明状态,会导致每次渲染时useState的调用顺序不一致,React 就会混淆不同状态的值,最终引发错误。

第一个版本的具体问题表现

isSentfalse时(初始状态),组件会进入else分支,执行const [message, setMessage] = useState(''),此时 React 会记录 “第 2 个状态是message”。

但当用户点击 “Send” 按钮后,isSent会被设置为true,此时组件会进入if分支,直接返回<h1>Thank you!</h1>不会执行else分支中的useState('')

这时候问题就出现了:

  • 第一次渲染时,React 记录了 “2 个状态”(isSentmessage)。
  • 第二次渲染时(isSenttrue),React 只找到了 “1 个状态”(isSent),状态的数量和顺序发生了变化。

React 会因此抛出错误(类似 “Rendered fewer hooks than expected”),因为它无法正确匹配状态的顺序。

第二个版本的修复:将状态声明移到顶层

第二个版本的代码将message状态的声明移到了组件的顶层作用域, outside 条件判断:

// 所有状态都在组件顶层声明,顺序固定
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('');if (isSent) {return <h1>Thank you!</h1>;
} else {// 直接使用已声明的 message 状态// ... 返回表单
}

这样修改后,无论isSenttrue还是false,每次渲染时:

  • useState的调用顺序都是固定的(先isSent,再message)。
  • 状态的数量也固定为 2 个,不会因为条件分支而变化。

React 就能始终正确追踪每个状态的值,避免了因状态顺序混乱导致的错误。

额外说明:未使用的状态是否有问题?

可能你会问:当isSenttrue时,message状态虽然被声明了,但并没有被使用,这会不会有问题?答案是不会。React 允许声明状态但暂时不使用,只要useState的调用顺序和数量在每次渲染时保持一致即可。未使用的状态只是占用一点内存,不会影响组件的正常运行。

总结:核心修复逻辑

第一个版本的问题在于在条件分支中声明状态(message,导致 React 无法通过固定的顺序追踪状态,引发渲染错误。

第二个版本通过将所有useState调用移到组件顶层,保证了每次渲染时状态声明的顺序和数量一致,符合 React 对钩子的使用规则,从而修复了错误。

这也是 React 钩子(Hooks)的核心规则之一:只在组件顶层调用 Hooks,不要在条件、循环或嵌套函数中调用

4 状态更新的 “异步性” 导致的预期不符

import { useState } from 'react';export default function FeedbackForm() {const [name, setName] = useState('');function handleClick() {setName(prompt('What is your name?'));alert(`Hello, ${name}!`);}return (<button onClick={handleClick}>Greet</button>);
}

改后

export default function FeedbackForm() {function handleClick() {const name = prompt('What is your name?');alert(`Hello, ${name}!`);}return (<button onClick={handleClick}>Greet</button>);
}

第一个版本的核心问题:状态更新的 “异步性” 导致的预期不符

第一个版本使用了useState来管理name状态,代码逻辑是:

  1. 点击按钮时,通过prompt获取用户输入的名字。
  2. 调用setName更新name状态。
  3. 立即用alert显示Hello, ${name}!

但这里存在一个关键问题:React 中状态更新是异步的。当你调用setName(newValue)时,React 并不会立即修改name变量的值,而是会安排一个状态更新任务,在合适的时机(通常是当前函数执行完成后)才会更新name并重新渲染组件。

这就导致了:

  • 当执行alert(Hello, ${name}!)时,name仍然是更新前的值(初始为空字符串'')。
  • 所以无论用户在prompt中输入什么,alert都会显示Hello, !(因为此时name还没被更新)。
  • 虽然状态最终会被更新(组件重新渲染时name会变为输入值),但alert的时机太早,无法获取到最新的状态。

第二个版本的修复:无需状态时避免使用状态

第二个版本删除了useState,直接在事件处理函数内部处理逻辑:

  1. 点击按钮时,通过prompt获取用户输入的名字,用普通变量name存储。
  2. 直接用alert显示Hello, ${name}!

这个修复的核心思路是:当不需要在组件渲染中使用某个值时,没必要把它存在 React 状态里

在这个例子中,name的值只在点击事件中使用一次(用于alert),后续组件渲染并不依赖这个值(没有在 JSX 中显示name)。所以:

  • 不需要用useState管理name,用普通变量存储临时值即可。
  • 普通变量的赋值是同步的,prompt获取值后,name会立即更新,alert能正确显示用户输入的名字。

总结:两个版本的对比和修复逻辑

第一个版本(有问题)第二个版本(修复后)
使用useState管理name状态不使用状态,用普通变量存储临时值
状态更新异步,alert无法获取最新值普通变量同步赋值,alert能获取最新值
多余地使用了状态(因为name不影响渲染)按需使用变量,逻辑更直接

修复的核心:React 状态的作用是 “存储影响组件渲染的值”,当一个值仅用于事件处理的临时计算,不参与 UI 渲染时,无需用useState,直接用普通变量即可避免状态更新异步导致的问题。

如果后续需求变化,比如需要在页面上显示用户输入的名字(例如<p>Hello, {name}</p>),这时就必须用useState管理name,但需要注意状态更新的异步性 —— 如果要在更新后立即做某事,需要用状态更新的回调函数,例如:

setName(newName => {alert(`Hello, ${newName}!`); // 这里能获取到最新值return newName;
});

但当前例子中没有这个需求,所以第二个版本的简化方案更合适。

useState的异步问题

我们结合前面那个FeedbackForm的例子,再具体说一下useState的异步问题是如何体现的,以及修复后的逻辑为什么能正常工作。

回顾第一个版本的代码(有问题)

const [name, setName] = useState('');function handleClick() {setName(prompt('What is your name?')); alert(`Hello, ${name}!`); // 这里的 name 是旧值
}
问题的关键过程:
  1. 用户点击按钮,handleClick函数执行。
  2. 首先调用prompt获取用户输入的名字(比如 “Alice”),然后调用setName("Alice")。此时,React 只是 “计划” 更新name状态,并不会立即把name''改成"Alice"(异步特性)。
  3. 紧接着执行alert(Hello, ${name}!),这里的name仍然是更新前的旧值(''),所以弹窗会显示Hello, !
  4. handleClick函数执行完毕后,React 才会真正更新name"Alice",并重新渲染组件。但此时alert已经执行完了,无法再获取到新值。

所以在这个例子中,“状态更新后立即读取” 的操作(alert)永远拿不到最新值,并不是 “有时能显示、有时不能”,而是一定不能显示最新值 —— 因为alert的执行时机在 React 处理状态更新之前。

第二个版本的代码(修复后)

function handleClick() {const name = prompt('What is your name?'); // 普通变量,同步赋值alert(`Hello, ${name}!`); // 直接用刚获取的变量
}
为什么能正常工作?

这里没有使用useState,而是用普通变量存储prompt的结果:

  1. prompt获取到用户输入(比如 “Alice”)后,立即赋值给普通变量name(同步操作,赋值后name就是"Alice")。
  2. 紧接着执行alert时,直接使用这个普通变量,自然能拿到最新值,弹窗会显示Hello, Alice!

因为这个场景中,name的值只需要在handleClick函数内部临时使用一次,不需要影响组件的渲染(JSX 中没有用到name),所以完全没必要用useState。用普通变量就能避开状态更新的异步问题,逻辑更直接。

延伸:如果需要在 UI 中显示name怎么办?

假设需求变了,不仅要弹窗,还要在页面上显示名字,这时候就必须用useState了。但要注意异步问题:

const [name, setName] = useState('');function handleClick() {const inputName = prompt('What is your name?');setName(inputName); // 更新状态// 此时 name 还是旧值,但没关系,因为 UI 会重新渲染
}// 组件重新渲染时,会使用更新后的 name
return (<div><button onClick={handleClick}>Greet</button>{name && <p>Hello, {name}!</p>} {/* 这里一定显示最新值 */}</div>
);

这种情况下:

  • handleClicksetName后,name虽然还是旧值,但p标签会在组件重新渲染后显示最新的name(因为 React 会在handleClick执行完后更新状态并重新渲染)。
  • 如果此时还需要在setName后立即执行某些操作(比如基于新name发请求),可以用setName的回调函数:
    setName(inputName => {// 这里的 inputName 是最新值console.log('最新名字:', inputName);return inputName;
    });
    

总结

在前面的例子中,第一个版本的问题是错误地在状态更新后立即读取旧值(因为useState异步更新),导致alert失效;第二个版本通过用普通变量存储临时值,避开了状态更新的异步问题,从而正常工作。

核心规律:当一个值不需要参与组件渲染时,无需用useState;当需要参与渲染时,尽管setXxx后立即读取是旧值,但重新渲染后的 UI 会显示最新值,必要时可通过setXxx的回调或useEffect处理更新后的逻辑。

【为什么是一定不能?】

因为 React 中 useState 的状态更新是 “异步且批量处理” 的,这种机制决定了在同一个事件处理函数里,调用 setXxx 之后立即读取原状态变量,一定只能拿到旧值,不可能拿到新值。

具体来说,第一个版本的代码执行顺序是 “固定死” 的:

function handleClick() {// 步骤1:调用 setName,告诉 React“计划更新 name”setName(prompt('What is your name?')); // 步骤2:此时 React 还没真正更新 name,所以这里的 name 还是旧值(初始为空)alert(`Hello, ${name}!`); 
}
  1. 步骤 1setName(...) 只是给 React 发了一个 “更新请求”,但 React 不会立刻执行这个更新(比如不会马上把 name 从 '' 改成用户输入的值)。这就像你在手机上发了一条消息,对方不会 “瞬间” 就看到并回复 —— 中间有一个 “传输和处理” 的过程。

  2. 步骤 2alert 执行时,React 还没处理完步骤 1 的 “更新请求”,name 变量的值仍然是更新前的旧值(初始为空字符串 '')。所以无论用户输入什么,alert 里的 name 都是空,必然显示 Hello, !

  3. 只有当 handleClick 函数完全执行结束后,React 才会处理之前的 “更新请求”,把 name 改成新值,然后重新渲染组件。但此时 alert 已经执行完了,再也没机会用到新值了。

为什么说 “一定不能”?

因为 React 的状态更新机制是 **“先收集所有更新请求,等当前函数执行完再统一处理”,而不是 “执行一个更新就立即生效”。在这个机制下,同一个函数里 “更新状态后立即读原变量” 的操作,本质上是 “在 React 还没处理更新时就去读结果”,所以100% 会拿到旧值 **,不存在 “有时候能拿到新值” 的可能。

这就像你在银行柜台提交了取款申请,还没等柜员处理,就想立即从账户里取出现金 —— 显然是不可能的,必须等柜员操作完成(函数执行结束),账户余额(状态)才会更新。

总结

第一个版本中,alert 之所以 “一定不能” 显示新值,是因为:setName 只是 “预约” 了更新,而 alert 在 “预约” 还没被处理时就执行了,此时状态根本没变化。这种 “先预约、后处理” 的异步机制,决定了同一函数内 “更新后立即读原变量” 的操作必然拿到旧值。

http://www.dtcms.com/a/575342.html

相关文章:

  • 怎么做能让网站尽快收录x wordpress 视差 主题
  • 建设网站哪个比较好wordpress 注册连接
  • 如何建设互联网政务门户网站wordpress 响应式 主题
  • 网站网页设计收费个人盈利网站怎么建立
  • 有没有好用的网站推荐c#网站开发模板
  • 建设网站虚拟主机淘宝网首页电脑登陆入口
  • 网站logo怎么做的中山网站建设模板网络公司
  • 常用的网站建设程序有哪些html在网站开发中的应用
  • 【Android】正式打包 Release 发布版本(创建秘钥,配置秘钥、打包签名)
  • 专业建站网网站运营推广24小时学会网站建设 百度云
  • 怎么查网站备案域名备案网店代运营收费多少钱
  • 基于树结构突破大模型自身能力
  • 蒙阴网站建设中山有哪些网站建立公司
  • Linux 内核——字符设备驱动框架详解
  • 毕业设计做网站还是系统湛江市手机网站建设企业
  • 做网站是否要备案网站建站网站
  • 莱芜做网站站酷网站
  • 上海加盟网网站建设如何做内网站的宣传栏
  • 如何设计公司标志图案江苏企业网站排名优化
  • 想要做一个网站关于政务网站建设工作情况的总结
  • 上海建网站工作室flash网站引导页面制作
  • 【Janet】语法与解析器
  • 异构比较查找
  • 网站价位无法访问服务器上网站
  • 服装购物网站排名icp备案证书
  • 网站对公司的作用是什么意思免费外贸网站制作
  • 一级a做爰片免费网站体验nginx进wordpress不能进目录
  • 湛江市建设局网站成功的软文营销案例
  • 对于网站开发有什么要求冉冉科技网站建设
  • 汝州网站建设网站建设方案数