我有一个非常简单的组件,看起来像这样:
它将列表作为输入,并允许用户在列表中循环。
该组件具有以下代码:
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
此组件的效果很好,除了状态更改后我还没有处理。
状态更改,我想将currentNameIndex
重置为零。
最好的方法是什么?
我考虑过的选项:
componentDidUpdate
此解决方案比较棘手,因为componentDidUpdate
在渲染后运行,因此我需要添加一个子句
在组件处于无效状态时,可以使用render方法“不执行任何操作”,如果我不小心的话,
我可以导致一个空指针异常。
我在下面包含了此实现。
getDerivedStateFromProps
getDerivedStateFromProps
方法为static
,签名仅使您可以访问
当前状态和下一个道具。这是一个问题,因为您无法确定道具是否已更改。如
结果,这迫使您将道具复制到状态中,以便您可以检查道具是否相同。
我不想这样做。该组件应私有拥有当前选择的索引。
我正在考虑采用这种方法,但不喜欢它如何导致父母需要了解 孩子的实施细节。
我花了大量时间阅读You Probably Don't Need Derived State 但对那里提出的解决方案不满意。
我知道这个问题的变化已经被问过多次了,但是我不认为任何答案都会权衡可能的解决方案。重复的一些示例:
使用componetDidUpdate
的解决方案(请参见上面的说明)
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
if(this.state.currentNameIndex >= this.props.names.length){
return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
}
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if(prevProps.names !== this.props.names){
this.setState({
currentNameIndex: 0
})
}
}
}
使用getDerivedStateFromProps
的解决方案:
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
copyOfProps?: Props
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
if( state.copyOfProps && props.names !== state.copyOfProps.names){
return {
currentNameIndex: 0,
copyOfProps: props
}
}
return {
copyOfProps: props
}
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
答案 0 :(得分:4)
正如我在评论中所说,我不喜欢这些解决方案。
组件不必关心父级在做什么,或者父级当前state
是什么,它们应该简单地吸收props
并输出一些JSX
,这样它们才是真正的可重用,可组合和隔离,这也使测试变得更加容易。
我们可以使NamesCarousel
组件包含轮播的名称以及该轮播的功能和当前的可见名称,并制作一个Name
组件,该组件仅做一件事,显示该名称通过props
要在更改项目时重置selectedIndex
,请添加一个useEffect
,并将项目作为依赖项,尽管如果仅将项目添加到数组的末尾,则可以忽略此部分
const Name = ({ name }) => <span>{name.toUpperCase()}</span>;
const NamesCarousel = ({ names }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [names])// when names changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
<Name name={names[selectedIndex]} />
<button onClick={next} disabled={selectedIndex === names.length - 1}>
Next
</button>
</div>
);
};
现在这很好,但是NamesCarousel
可重用吗?不,Name
组件是,但是Carousel
与Name
组件耦合。
那么我们该怎么做才能使其真正可重用并看到单独设计组件的好处?
我们可以利用render props模式。
让我们制作一个通用的Carousel
组件,该组件将使用items
的通用列表并调用children
函数,以传递所选的item
const Carousel = ({ items, children }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [items])// when items changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
{children(items[selectedIndex])}
<button onClick={next} disabled={selectedIndex === items.length - 1}>
Next
</button>
</div>
);
};
现在这种模式实际上给我们带来了什么?
它使我们能够呈现这样的Carousel
组件
// items can be an array of any shape you like
// and the children of the component will be a function
// that will return the select item
<Carousel items={["Hi", "There", "Buddy"]}>
{name => <Name name={name} />} // You can render any component here
</Carousel>
现在它们既是隔离的又是真正可重用的,您可以将items
作为图像,视频甚至用户的数组来传递。
您可以更进一步,为轮播提供要显示为道具的商品数量,并使用一系列商品来调用子功能
return (
<div>
{children(items.slice(selectedIndex, selectedIndex + props.numOfItems))}
</div>
)
// And now you will get an array of 2 names when you render the component
<Carousel items={["Hi", "There", "Buddy"]} numOfItems={2}>
{names => names.map(name => <Name key={name} name={name} />)}
</Carousel>
答案 1 :(得分:2)
可以使用功能组件吗?可能会简化一些事情。
import React, { useState, useEffect } from "react";
import { Button } from "@material-ui/core";
interface Props {
names: string[];
}
export const NameCarousel: React.FC<Props> = ({ names }) => {
const [currentNameIndex, setCurrentNameIndex] = useState(0);
const name = names[currentNameIndex].toUpperCase();
useEffect(() => {
setCurrentNameIndex(0);
}, names);
const handleButtonClick = () => {
setCurrentIndex((currentNameIndex + 1) % names.length);
}
return (
<div>
{name}
<Button onClick={handleButtonClick}>Next</Button>
</div>
)
};
useEffect
与componentDidUpdate
相似,其中它将一个依赖项数组(状态变量和prop变量)作为第二个参数。当这些变量更改时,将执行第一个参数中的函数。就那么简单。您可以在函数体内进行其他逻辑检查以设置变量(例如setCurrentNameIndex
)。
请小心,如果在函数内部更改了第二个参数的依赖项,那么您将拥有无限的回报。
签出useEffect docs,但是在习惯了钩子之后,您可能永远不想再使用类组件。
答案 2 :(得分:1)
您问什么是最好的选择,最好的选择是使其成为受控组件。
该组件的层次结构太低,以至于不知道如何处理其属性更改-如果列表更改了但只是稍稍更改(也许添加了新名称)该怎么办-调用组件可能想要保持原始位置。
在所有情况下,如果父组件可以决定在提供新列表时组件的行为方式,那么我认为我们会更好。
这样的组件也可能是更大整体的一部分,需要将当前选择传递给它的父对象-也许作为表单的一部分。
如果您真的不希望将其设为受控组件,则还有其他选择:
useEffect
是一种非常干净的方法。getDerivedStateFromProps
-是的,这意味着在状态中保留对名称列表的引用并进行比较。如果这样写,它看起来会更好一些: static getDerivedStateFromProps(props: Props, state: State = {}): Partial<State> {
if( state.names !== props.names){
return {
currentNameIndex: 0,
names: props.names
}
}
return null; // you can return null to signify no change.
}
(如果选择此路线,您可能还应该在render方法中使用state.names
)
但实际上-受控制的组件是必经之路,您可能迟早会在需求发生变化且父项需要知道所选项目的情况下这样做。