React 学习笔记
以此记录我在 React beta版的官方文档中所学的内容
一、Quick Start 快速上手
1.组件基础
React 组件是JS函数并且返回标记性语言
function MyButton() {
return (
<button>I'm a button</button>
);
}
你可以在其他组件中引入你刚写的组件
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
);
}
注意: React组件必须以大写字母开头,而 HTML 标签必须小写
export default 关键词用于指定文件中的主要组件
2.JSX
我们所使用的标记语法被称为JSX,它是可选的,但是出于便捷,大多数react工程都使用JSX。JSX比HTML更严格
- 所有HTML标签都需要闭合标签
- return的内容不能是多个标签,可以用空标签<>..</>将其多个标签包裹打包
小技巧:如果你有一大堆HTML要移植到JSX,可以使用在线转换工具,将其转换为JSX语法HTML to JSX (transform.tools)
语法规则总结:
- 需要指定类名时用 calssName 来添加
- react并没有规定如何添加CSS文件,简单的可以用link标签。如果使用了工具或者框架,需要去查看他们的文档来知道如何添加
- JSX中通过{}来嵌入变量,不仅是在内容区域,还可以在给属性赋值时
- 在style中采用 { {} },嵌入对象变量来添加样式
if条件写法:
第一种:在return外写好逻辑
```
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return (
<div>
{content}
</div>
);
```
第二种:紧凑写法
<div>
{isLoggedIn ? (
<AdminPanel />
) : (
<LoginForm />
)}
</div>
如果不需要else的话可以这样写
<div>
{isLoggedIn && <AdminPanel />}
</div>
map()函数遍历
假设你有一个数组,内部有许多对象
const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
数组的 .map 方法,将数组里 的元素转换成一个个 li
const listItems = products.map(product =>
<li key={product.id}>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
key 属性值应该是独一无二的,方便 react 在增删后标识元素,一般来说key来自于数据库里的id
事件处理函数
需要注意的是,在{}结尾处没有圆括号(),只需要在外声明好函数,并向下传递它,React将调用你的事件处理程序 onClick={handleClick}
function MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
更新和存储状态
如果需要存储某些变量值并展示它们,那么将会用到 useState
import { useState } from 'react';
现在可以通过数组结构赋值来声明一个状态变量
function MyButton() {
const [count, setCount] = useState(0);
数组结构赋值,你可以修改变量count和方法setCount的名字为任何名,但通常为 [sonmthing,setSomething],注意顺序不能变,第一个变量count永远对应着当前状态,第二个setCount则是提供的方法用于改变状态值,在useState()中可以初始化状态值
你无法直接修改状态值,只能通过提供的方法来修改
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
组件内声明的状态会单独存在,多个重复组件被调用时,其内状态独立
import { useState } from 'react';
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
但是我如果想要两个按钮共享同一状态,则需要进行状态上升,上升到两组件最近的一父组件
第一步,将状态上升
function MyButton() {
// ... we're moving code from here ...
}
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
然后将状态向下传递到MyApp里的每个MyButton
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update together</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
这里直接通过{}向子组件传递了信息,称作props。我们需要在子组件里去接收这些信息。这里通过对象的方式来接收,保证名字一致对应
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
Clicked {count} times
</button>
);
}
状态上升的思想可以归纳成,将子组件本身的状态去掉改为接收状态,然后子组件共有的父组件里去声明状态并传递给每一个需要共享状态的子组件使用
3.Using Hooks
以use单词为首开始的函数被称作为Hooks。useState 是React提供的内置Hook,可以在React API reference查阅更多内置的Hook,你也可以自己写Hook通过已经存在的Hooks
Hooks语法比一般函数更加严格,只能在组件或者其他Hook的顶层调用Hooks。如果要在条件或者循环里使用,useState提取新组件并将其放在那里
二、Thinking in React
在给定json数据的时候,如何考虑构建UI
[
{ category: "Fruits", price: "$1", stocked: true, name: "Apple" },
{ category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },
{ category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },
{ category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },
{ category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },
{ category: "Vegetables", price: "$1", stocked: true, name: "Peas" }
]
- 将UI分解为组件等级
- 理想状态下组件应该只做一件事,如果在编写过程中有所增加,则应该分解成更小的子组件
- CSS考虑你需要的类来生产选择
- 设计上组织和分层
- 查找 UI 状态的最小但完整的表示形式
将状态视为应用需要记住的最小更改数据集。构建状态的最重要原则是保持干燥(不要重复自己)。找出应用程序所需状态的绝对最小表示形式,并按需计算其他所有内容。例如,如果您正在构建购物清单,则可以将项目存储为处于状态的数组。如果还希望显示列表中的项目数,请不要将项目数存储为另一个状态值,而是读取数组的长度。
注意区分状态:
- 一直没有变化的不是state
- 通过父组件传递的props不是state
- 能够通过组件已有的state或者props计算得出的值绝对不是state
注意区分state和props,props为传递为组件的参数,而state则像是组件本身的记忆,它能够让组件跟踪某种信息并在产生交互的时候改变他们
- 明确state的使用地方
*记住,react 是单向数据流,数据通过组件等级制度由父亲传递给子组件
- 确认每个组件基于state呈现的东西
- 找到最近的共同父节点
- 决定state存在地方:
你可以直接将state放入共同的父节点里
你也可以将state放入共同父节点之上的某个节点
如果找不到一个需要拥有状态的组件,请创建一个仅用于保存状态的新组件,并将其添加到公共父组件上方的层次结构中的某个位置
- 添加反转数据流
如果我们需要通过子组件改变父组件的状态,那么需要将set方法也传入到子组件中
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
在SearchBar中,添加onChange事件处理函数来调用set方法更改状态值
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />
三、UI描写
React is a JavaScript library for rendering user interfaces (UI). UI is built from small units like buttons, text, and images. React lets you combine them into reusable, nestable components. From web sites to phone apps, everything on the screen can be broken down into components. In this chapter, you’ll learn to create, customize, and conditionally display React components.
此处会省略掉之前已提到的部分,包括传参,{}引用
-
jsx 导入导出 通过 export defaut function name(){xx} 和 import name from '..' 来导出和导入。普通js文件则通过export和import
-
保持纯组件:
一个纯函数应该具备
- 只关注自身的业务。不该改变任何未被调用的变量或者对象
- 给定输入,输出结果也是确定的。保证同样的输入,同样的结果
四、添加交互
三步添加一个简单交互:
- 声明事件触发函数handleClick在组件内部
- 在函数里完善逻辑
- 给事件源添加触发
export default function Button() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
事件处理函数应具备:
- 通常定义在组件内部
- 名字以handle开头,后跟着事件名
注意,函数传递事件处理时必须是被传递,而不是调用。这也意味着<button onClick={handleClick}> 不需要()。和下面行内声明同时调用的方式区别开
可以在行内直接声明函数并调用
<button onClick={function handleClick() {
alert('You clicked me!');
}}>
或者更简洁的一种写法,通常行内声明事件为那些简短函数
<button onClick={() => {
alert('You clicked me!');
}}>
我们也可以将函数handleClick作为参数传入到子组件中
function AlertButton({ message, children }) {
return (
<button onClick={() => alert(message)}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div>
<AlertButton message="Playing!">
Play Movie
</AlertButton>
<AlertButton message="Uploading!">
Upload Image
</AlertButton>
</div>
);
}
我们还可以设置一些基础组件,如按钮,它接收父组件传递的onClick函数和内容来实现不同的复用,如上传按钮,确定按钮等,在上传按钮里调用基础按钮组件并传入上传的事件处理函数。
事件处理函数作为参数时命名
一些内置的组件如button和div只支持浏览器规定的固定的事件名,如onClick。当是你自己的组件,你可以使用其他名字而不受限制,当然建议还是以on开头,后面紧跟事件名字。
如下,在自己的组件Button中传入了自己命名的事件处理函数onSmash,但在其内部button中仍然是onClick去接收
function Button({ onSmash, children }) {
return (
<button onClick={onSmash}>
{children}
</button>
);
}
export default function App() {
return (
<div>
<Button onSmash={() => alert('Playing!')}>
Play Movie
</Button>
<Button onSmash={() => alert('Uploading!')}>
Upload Image
</Button>
</div>
);
}
这样的好处是,当需要向一个组件传递多个事件处理函数时,我们可以命名为有意义的名字并且子组件可以根据名字来接收使用。例如提到的上传和播放功能
事件传播
子组件的事件会冒泡,进而触发父组件里的事件,如果要阻止冒泡默认行为则在事件处理函数中调用e.stopPropagation()
除了onScroll事件,所有事件都会在React中传播
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<Button onClick={() => alert('Playing!')}>
Play Movie
</Button>
<Button onClick={() => alert('Uploading!')}>
Upload Image
</Button>
</div>
);
}
注意这里子组件Button里接收来自父组件的onClick事件处理函数,但由于需要阻止冒泡调用e,在return的button里是这样写的,
onClick={ e => {} } 而不是 onClick={onClick} 注意二者的区别,前者是在方括号里补充函数部分,后者是传递一整个事件处理函数,后者是不需要再添加()表调用的,而前者花括号里面需要再去调用父组件传来的函数。
When you click on a button:
-
React calls the
onClickhandler passed to<button>. -
That handler, defined in
Button, does the following:- Calls
e.stopPropagation(), preventing the event from bubbling further. - Calls the
onClickfunction, which is a prop passed from theToolbarcomponent.
- Calls
-
That function, defined in the
Toolbarcomponent, displays the button’s own alert. -
Since the propagation was stopped, the parent
<div>’sonClickhandler does not run.
阻止默认行为
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('Submitting!');
}}>
<input />
<button>Send</button>
</form>
);
}
通过调用 e.preventDefault() 阻止了表单内按钮被点击时,表单内的默认行为(重置整个页面)
e.stopPropagation()stops the event handlers attached to the tags above from firing.e.preventDefault()prevents the default browser behavior for the few events that have it.
事件处理程序的副作用
事件处理程序是绝佳的副作用使用地方,事件处理不需要保持纯函数,因此用于去改变一些事情。例如,当打字时改变输入框的值,按下按钮改变表单的内容。当然要改变某些信息之前首先要存储它们,这被称作为状态state,组件的记忆
五、管理状态
在Rect使用状态的思路:
- Identify your component’s different visual states
- Determine what triggers those state changes
- Represent the state in memory using
useState - Remove any non-essential state variables
- Connect the event handlers to set the state
1.识别组件的不同视觉状态
首先,需要识别所有接触到UI的人可能会看见的不同状态。如下针对一个表单可能有的状态:
- Empty: Form has a disabled “Submit” button.
- Typing: Form has an enabled “Submit” button.
- Submitting: Form is completely disabled. Spinner is shown.
- Success: “Thank you” message is shown instead of a form.
- Error: Same as Typing state, but with an extra error message.
通过模拟状态,可以在连接任何逻辑之前快速迭代UI
2.确定状态改变的触发器
触发状态更新的方式有两种:
- 人工输入,像点击按钮
- 机器输入,像网络请求得到回应
这两种输入情况下都要设置状态变量来更新UI
将表单的数据流可视化并将触发器用箭头连接起来
3.用useState来记录状态
注意简化,状态的每一部分都是流动的,你需要尽量少的流动部分。越复杂将会导致越多的bug
对于表单组件来说,首先确定必须的状态,如输入的值和返回的错误信息
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
然后将你十分确定的可能出现的视觉状态列出
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
这可能并不是最好的,还需要进行优化
4.去除任何无关紧要的的状态变量
目的是避免存储的状态不展示任何有效的UI(你想让用户看到的UI) 通过几个问题可以很好的去除冗余项:
-
这种状态是否会产生悖论?比如isTying和isSubmitting不可能同时为true。悖论的出现通常意味着状态没有足够的约束。isTying和isSubmitting两个布尔值之间有四种组合,然而只有三种是有效的(true和true为悖论)。为了去除这种不可能的状态,将这三种状态归纳为一种状态status,status的值为 'typing' or 'submitting' or 'success'
-
相同的信息是否已经在另一个状态变量中可用?另一个悖论,isEmpty和isTyping不可能同时为true。这两个状态分离,将会导致不同步和犯错的风险。因此考虑移除isEmpty,用answer.length === 0 来表达相同意思
-
你能在另一个状态变量的反函数中获取相同的信息吗?isError并不需要,因为你可以检查已有的状态变量
error !== null来达到相同效果
最终表单的状态:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
5.最后连接起来,利用状态转移所需的触发器
import { useState } from 'react';
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');
if (status === 'success') {
return <h1>That's right!</h1>
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
Submit
</button>
{error !== null &&
<p className="Error">
{error.message}
</p>
}
</form>
</>
);
}
function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== 'lima'
if (shouldError) {
reject(new Error('Good guess but a wrong answer. Try again!'));
} else {
resolve();
}
}, 1500);
});
}
尽管此代码比原始命令式示例长,但它的脆弱性要小得多。通过将所有交互表示为状态更改,可以在以后引入新的视觉状态,而不会破坏现有状态。它还允许您更改在每个状态下应显示的内容,而无需更改交互本身的逻辑。