React状态管理

 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 (...);
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值