6.1 构建一个星级评分组件
import { FaStar } from "react-icons/fa" export default function StarRating() {
return [ <FarStar color="red" /> <FarStar color="red" /> <FarStar color="red"
/> <FarStar color="grey" /> <FarStar color="grey" /> ] }
下面来构建一个组件,根据selected属性自动填充颜色。
const createArray = length => [...Array(length)]; export default function
StarRating({totalStars = 5}){ return createArray(totalStars).map((n, i)=>{
return <Star key={i} /> }); }
6.2 useState 钩子
把组件变成可点击,使用React状态存储和修改它的值,状态通过React的钩子特性纳入函数组件。这里使用useState给组件添加状态。
export devault function StarRating({totalStars}){ const [selectedStars] =
useState(3); return ( <> { createArray(totalStars).map((n, i)=>{ <Star key= {i}
selected={selectedStars > i} /> }) } <p> {selectedStars} of {totalStars} stars
</p> </> ) }
useState是一个钩子函数,调用后返回一个数组。数组中的第一个值是我们想使用的状态变量。状态变量是selectedStars。既然useState返回一个数组,那我们九可以使用数组析构把状态变量命名为任何名称。
为了收集用户的评分,我们要让用户能点击各个星标。也就是说,我们要为FaStar组件添加一个onClick处理程序,把星标变成可点击的。
const Star = ({selected=false, onSelect=f=>f}) => { <FaStar color={selected ?
"red" : "grey"} onClick={onSelect} /> }
这里为星标添加了一个onSelected属性,注意,这个属性是一个函数。用户店家FaStar组件时,我们可以调用这个函数,通知夫组件该星标被点击了。这个属性默认值时f=>f,这个时一个虚假函数,什么也不做,只是返回传入的参数。然而,倘若不设置一个默认函数,onSelect属性就是未定义的,点击FaStar组件时将报错,因为onSelect的值必须是一个函数。虽然f=>f什么也没做,但它任然是一个函数,调用时不会报错。不定义onSelect属性也没有关系,react会调用那个虚假的函数,什么也不会发生。
export devault function StarRating({totalStars}){ const [selectedStars,
setSelectedStars] = useState(3); return ( <> { createArray(totalStars).map((n,
i)=>{ <Star key= {i} selected={selectedStars > i}
onSelect={()=>{setSelectedStars(i+1)}} /> }) } <p> {selectedStars} of
{totalStars} stars </p> </> ) }
使用钩子时有一件事情一定要记住:钩子会导致所在组件重新渲染。每次调用setSelectedStars函数修改selectedStars的值,
useState钩子都会调用StarRating函数组件,使用selectedStars的值新重新渲染,钩子函数的强大就在这里。
钩子中数据发生变化之后,钩子会使用新的数据重新渲染所在的组件。
6.3 为提高可重用性而重构
现在Star组件可以放到线上使用了。只要想从用户那里收集评分,你的应用九可以使用这个组件。然后,如果你想把这个组件发布到npm中,供世界上其他人使用,从用户哪里手机评分,获取应该考虑再多处理几种情况。
首先考虑style属性。这个属性的作用是为元素添加CSS样式。其他开发者,甚至于你自己,以后很有可能需要修改整个容器的样式。届时,你可能会这么做:
export default function App(){ return <StarRating style={{backgroundColor:
"lightblue"} /> }
其他最好的做法是把样式传给StarRating容器。目前,StarRating没有容器,用的是一个React片段。为此,我们要把片段升级成一个div元素,然后把样式传给该元素。
export default function StarRating({style={}, totalStars}){ const
[selectedStars, setSelectedStars] = useState(3); return ( <div style={{padding:
"5px", ...style}} > { createArray(totalStars).map((n, i)=>{ <Star key= {i}
selected={selectedStars > i} onSelect={()=>{setSelectedStars(i+1)}} /> }) } <p>
{selectedStars} of {totalStars} stars </p> <div/> ) }
此外,有些开发中可能会想为整个星级评级系统实现其他的常用属性:
export default function App(){ return <StarRating style={{backgroundColor:
"lightblue"} onDoubleClick={e=>alert()} /> } export default function
StarRating({style={}, totalStars, , ...prop}){ const [selectedStars,
setSelectedStars] = useState(3); return ( <div style={{padding: "5px",
...style}} {...prop}> { createArray(totalStars).map((n, i)=>{ <Star key= {i}
selected={selectedStars > i} onSelect={()=>{setSelectedStars(i+1)}} /> }) } <p>
{selectedStars} of {totalStars} stars </p> <div/> ) }
首先,假设用户只会添加这个div支持的属性。其次,假设用户无法为组件添加恶意属性。
这个条规则不使用于所有的组件。其实在某些情况下最后只提供这种疾病的支持。本节的要点是让你知道,应该为以后可能使用组件的用户考虑。
6.4 组件书中的状态
在每个组件中使用状态不是个太好的注意。状态数据分散在多个组件中不便于追踪bug和修改应用,因为你很难确定状态值在组件树中具体的位置。如果集中在一处管理,你对应用的状态或某个功能更的状态讲有更好的掌控。
第一种做法是在状态书的根不存储状态,通过属性把状态传给子组件。
我们来编写一个小型应用,用于报错颜色列表。如下所示:
[ { id: "1", title: "ocean at dusk", color: "#00c4e2", rating: 5 } ]
6.4.1 沿组件树向下发送状态
我们把状态存储在应用的根组件,然后把颜色向下传给子组件,负责渲染。
import colorData from "./color-data.js" import ColorList from ".ColorList.js"
export default function App() => { const [colors] = useState(colorData) return
<ColorList colors={colors} /> }
纯组件指不含状态的函数组件。
6.4.2 沿组件树向上发送交互
import {FaTrash} from "react-icon/fa"; export default function Color({id,
title, color, rating, onRemove=f=>f}) { return ( <section> <h1>{title}</h1>
<button onClick={()=>{onRemove(id)}}> <FaTrash /> </button> <div style={
{height:"50px", background: color}} /> <StarRating selectedStars={rating} />
</section> ) } export default function ColorList({colors = [],
onRemoveColor=f=>f}){ if(!colors.length) return <div>No Colors Listed. return (
colors.map(color=>{ <Color key={color.id} {...color} onRemove={onRemoveColor}
/> }); ); }
6.5 构建表单
6.5.1 使用ref
import {useRef} from react; export default function AddColorForm({}){ const
txtTitle = useRef(); return ( <form> <input ref={txtTitle} type="text"/>
</form> ); }
6.5.2 受控组件
在受控组件中,表单指有React管理,而非DOM。此时,无需用ref,不用编写命令式代码,添加可好的表单验证等功能也要容易得多。
import {useState} from react; export default function AddColorForm({}){ const
[title, setTitle] = useState(""); return ( <form> <input value={title}
type="text" onChange={event=>setTitle(evant.target.value)} /> </form> ); }
这个组件之所有叫受控组件,是因为React控制这表单的状态。值得注意的是受控的表单组件经常重新渲染,想象一下,在title字段中输入一个字符,AddColorForm就会重新渲染。React禁受得住这种考验。希望知道受控组件会频繁重新渲染组件后,你能避免现在受控组件中添加耗时、耗资源的操作。在优化React组件时,你至少能合理的运用这个知识。
6.5.3 自定义钩子
如果一个表单有大量的input元素,你肯定会不断复制粘贴下面的两行代码:
value={title} onChange={event=>{setTitle(event.target.value)}}
复制粘贴代码,表明代码时重复的,应该提取出来定义成函数。
我们可以把创建受控表单组件的详细过程独立打包,定义为一个钩子。我们可以自定义一个useInput钩子,去除创建受控的表单输入框过程中涉及的重复。
import {useState} from "react" export const useInput = initialValue => { const
[value, setValue] = useState(initialValue ); return [ {value, onChange:e=>
setValue(e.target.value)}, () => setValue(initialValue ) ] }
这是一个自定义的钩子,实现的代码并不多。在这个钩子函数内部,我们任然使用了useState钩子创建状态值。然后,返回一个数组。数组中的第一个值是一个对象:包含我们之前复制的代码,状态中的value和用于修改状态值的onChange函数属性。数组中的第二个值是一个函数,他的作用是把value重置为初始值。下面再AddColorForm中使用这个钩子。
import {useState} from react; import {useInput} from "./hook"; export default
function AddColorForm({}){ const [titleProps, resetTitle] = useInput(""); const
[colorProps, resetColor] = useInput(""); return ( <form onSubmit={submit}>
<input ...titleProps type="text" /> <input ...colorProps type="color" />
</form> ); } const submit = event => { event.preventDefault();
onNewColor(titleProps.value, colorProps.value); restTitle(); restColor(); }
钩子应该再React组件内部使用。在自定用义的钩子内可以使用其他的钩子,毕竟钩子最终要在组件中使用。在我们自定用的钩子中,如果状态发生了变化,任会导致AddColorForm重新渲染,呈现titleProps或colorProps的新值。
6.5.4 把颜色添加到状态中
const [colors, setColors] = useState(colorData) const onNewColor = { (title,
color)=>{ const newColors = [ ...colors, { id: v4(), rating: 0, title, color }
] setColors(newColors); } }
6.6 React上下文
在React早期版本中,把状态集中放在组件树的根部是一个重要的模式,让我们受益良多。作为react开发者,我们应该掌握如何通过属性在组件树中向下和向上传递状态。然后随着React的发展,以及组件树枝繁叶茂,遵守这个原则慢慢的变得不切实际。对于很多开发者来说,在复杂应用中集中于组件的根部维护状态不是一件容易的事。在众多组件之间向下向上传递状态,操作繁琐,而且容易出错。
状态数据通过属性有一个个组件传递,一直传到需要使用状态的组件,这个就好像从旧金山坐火车到华盛顿,途径很多州,到终点站才下车。显然从旧金山乘坐飞机到华盛顿更便捷。这样不同经停各州,而是直接飞过。
在react中,上下文(context)就像是数据的飞行装备。为了把数据放入react上下文,我们需要创建上下文供应组件(context
provide)。这是一种react组件,可以包含整个组件树,也可以包含组件树的特定部分。
上下文供应组件是始发港,数据在这里登机。上下文组件也是枢纽,所有的航班都从这里离岗,飞往不同的目的地。各个目的地是上下文消费组件(context
customer),即从上下文中获取数据的react组件。
6.6.1 把颜色值放入上下文
React提供的createContext函数创建上下文对象,这个对象包含两个组件:Provider和Customer。
import {createContext} from "react"; export const ColorContext =
createContext(); render( <ColorContext.Provider value={colors}> <App />
</ColorContext> )
6.6.2 使用useContext获取颜色
useContext钩子用于从上下文中获取值,它从上下文Consumer中获取我们需要的值。
import {useContext} from "react"; import {ColorContext} from "./" export
default function ColorList(){ const { colors } = useContext(ColorContext); ... }
使用上下文消费组件
import {ColorContext} from "./" export default function ColorList(){ const {
colors } = useContext(ColorContext); return ( <ColorContext.Customer> { context
=> { if(context.colors){ ... } } } </ColorContext.Customer> ) }
6.6.3 有状态的上下文供应组件
上下文供应组件可以把把对象放入上下文,但是无法修改上下文的值,需要父级组件的协助。
import { createContext, useState} from "react"; import colorData from
"./color-data.json"; const ColorContext = createContext(); export default
function ({children}){ const [colors, setColors] = useState(colorData); return
( <ColorContext.Provider value={{colors, setColors}}> {children}
</ColorContext.Provider> ); }
6.6.4 使用上下文自定义钩子
还有一个重要的地方需要修改,引入钩子之后,我们完全不用把上下文开放给消费组件。我们可以把上下文包装到一个自定用得钩子函数中,我们在在对外开放ColorContext实列,而是创建一个名为useColors的钩子,从上下文中返回颜色。
我们把渲染和处理有状态的颜色所需的功能全部功能打包到一个javascript模块中,上下文隐藏在这个模块中,通过一个钩子对外开发。
import { ColorProvider } from "./color-hooks.js" render( <ColorProvider> <App
/> </ColorProvider> ) import { useColors } from "./color-hooks.js" export
default function ColorList(){ const { colors } = useColors(); return (...); }