React - 当 Axios 在单独的组件中调用时,setState 不会重新渲染页面

Posted

技术标签:

【中文标题】React - 当 Axios 在单独的组件中调用时,setState 不会重新渲染页面【英文标题】:React - setState not re-rendering page when Axios call in separate component 【发布时间】:2020-06-15 15:24:20 【问题描述】:

我正在做一些研发工作,并决定学习 React 并在此过程中构建应用程序。

我遇到了一个奇怪的问题,这让我摸不着头脑。我已经在 *** 上找不到与我的问题完全匹配的问题,所以我想发个帖子。

问题的真相:在我的组件外部进行 Axios 调用然后设置状态不会触发重新渲染,但在组件内进行 Axios 调用会触发。

我已经重命名了部分代码。

应用总结

我已经创建了 MVC 应用程序的前端部分,它将与我同事的 Spring Boot 服务器对话以检索“配置”数据,这些数据将从 SQL 数据库中检索。我的前端将显示此配置数据并允许对其进行编辑。

我正在使用 Axios 进行 REST 调用。

我在我的一个组件的 componentDidMount() 中有一个 Axios 调用,它对服务器进行初始调用,然后将这些结果添加到该组件的状态。然后页面重新呈现,并用结果填充表格。该表是它自己的组件,每个条目也是它自己的组件。数据通过 props 传递给这些组件。

问题

如果我将 Axios 调用保留在 componentDidMount() 中,那么我的应用程序将按预期工作。

但是,我有多个进行 REST 调用的组件,因此存在重复的代码,所以我想在我的应用程序中创建一个任何组件都可以调用的 REST 服务。我通过遵循命令设计模式来做到这一点,我什至用 Typescript 编写了它。我知道矫枉过正,但这是为了研发,我只是想把它作为一个学习练习。

所以要使用它,我的组件中有一个函数,它设置命令、带有命令和 url 的调用程序,然后在调用程序上调用执行并将结果添加到变量中。

所以问题来了:如果我使用我的 REST 服务,它在我的组件之外的一个类中调用了 Axios,那么不会发生重新渲染并且我的表中没有填充数据。但是,如果我将 Axios 调用保留在我的 componentDidMount() 中,那么一切正常。

我的尝试

在玩了很多 console.logs 之后,我看到 setState 在设置状态之前没有获取数据,所以我的状态中的“项目”是空白的。我通过在函数中添加“async”和在 setState 调用中添加“await”来解决这个问题。现在我可以通过 console.log 看到状态有数据,但是重新渲染仍然没有触发。

我也尝试过移动一些东西 - 所以我在 componentDidMount() 中拥有了函数和所有内容,然后我将所有内容移出并简单地在 componentDidMount() 中调用了该函数 - 这没有奏效。

我的代码

maincomponent.js 这是发起 Axios 调用的组件。你会看到我原来的 Axios 调用被注释掉了。

class ParameterList extends React.Component 
constructor(props) 
    super(props)
    this.state = 
        error: null,
        isLoaded: false,
        items: [],
    


componentDidMount() 
    // Axios.get(URLS.GET_CONFIG_PARAMETERS).then((results) => 
    //     console.log(results.data)
    //     this.setState(
    //         items: results.data
    //     )
    // )

    this.retrieveCONFIGParameters()



async retrieveCONFIGParameters() 
    const serviceCommand = new GetServiceCommand()
    const invoker = new ServiceInvoker(serviceCommand, URLS.GET_CONFIG_PARAMETERS)
    const returnData = invoker.ExecuteRequest()

    await this.setState(
        items: returnData
    , function() 
        console.log(this.state.items)
    );  

    console.log(this.state.items)


render() 
    return (
        <div>
            <h1>Parameter List</h1>
            <Container>
            <Grid celled>
                <Grid.Row>
                <Label attached='top left' color='blue'>Current EDI Message Status</Label>
                    <Grid.Column width=16>
                    <Table celled structured color='blue'>
                        <Table.Header>
                            <Table.Row>
                                <Table.HeaderCell><Radio label='Active'></Radio></Table.HeaderCell>
                                <Table.HeaderCell><Radio label='Inactive'></Radio></Table.HeaderCell>
                                <Table.HeaderCell><Radio label='All'></Radio></Table.HeaderCell>
                            </Table.Row>
                            <Table.Row>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            <Table.HeaderCell>XXXX</Table.HeaderCell>
                            </Table.Row>
                        </Table.Header>
                            <Table.Body>
                                    this.state.items.map((param, i) => 
                                        if (param.active === true) 
                                            return <ParameterListItem key=i paramData=param />
                                         else 
                                            return false // TODO
                                        
                                    
                                )
                            </Table.Body>
                    </Table>
                    </Grid.Column>
                </Grid.Row>
                <Grid.Row>
                    <Grid.Column width=16>
                    <Button color='teal'>
                        <Button.Content>Add</Button.Content>
                    </Button>
                    <Button color='teal'>
                        <Button.Content>Modify</Button.Content>
                    </Button>
                    <Button color='teal'>
                        <Button.Content>Remove</Button.Content>
                    </Button>
                    <Button color='teal'>
                        <Button.Content>Print</Button.Content>
                    </Button>
                    <Button color='teal'>
                        <Button.Content>Position</Button.Content>
                    </Button>
                    <Button color='teal'>
                        <Button.Content>Re-Instate</Button.Content>
                    </Button>
                    </Grid.Column>
                </Grid.Row>
            </Grid>
            </Container>
            <br></br>
        </div>
    );


GetServiceCommand.ts 这是调用 Axios 的 GET 命令。

export class GetServiceCommand implements IServiceCommand 
returnData = [] as any
execute(serviceURL: string): Object 
    Axios.get(serviceURL).then(results => 
        console.log(results.data)
        results.data.forEach((element: any) => 
            this.returnData.push(element)
        );
    )
    console.log(this.returnData)
    return this.returnData

如有必要,我可以提供其他类的代码,但它们只是标准的命令模式类,所以我认为不需要。

最后的想法

我知道我所做的可能是矫枉过正,而回到简单地在每个组件中使用 Axios 调用将使这个问题消失。但是这几天一直在我的脑海里,我很想知道我做错了什么。

最后一件事:来自 Axios 调用和状态的数据的 console.log 看起来不同。从 Axios 调用本身来看,数据如下所示:

(6) […, …, …, …, …, …]
0: code: "8", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
1: code: "5", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
2: code: "2", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
3: code: "6", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
4: code: "6", active: false, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
5: code: "6", active: false, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
length: 6

从状态:

[]
0: code: "8", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
1: code: "5", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
2: code: "2", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
3: code: "6", active: true, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
4: code: "6", active: false, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
5: code: "6", active: false, description: "Sample Param Desc", XXXX: "System1", networkId: "Network1", …
length: 6

您会注意到状态以 [] 开头,而 Axios 数据是 (6)。我不确定这是否是导致问题的原因......我认为这不太可能,因为它们只是数组,但我想我会提到它以防万一。

感谢任何花时间查看此内容的人。

编辑

感谢所有提供建议的人。我现在已经设法解决了这个问题,虽然感觉有点脏。

这些建议强调了 Axios 调用和返回的问题 - 在整个节目中花了一些时间处理大量的 console.logs 之后,我开始意识到 GetServiceCommand 中的 Return 实际上是空白的;这很令人困惑,因为其他 console.logs 显示它一旦进入组件就会有数据,但这不可能是正确的,因为 Return 在数据添加到数组之前触发。我认为 console.logs 本质上误导了我(或者更准确地说,我误解了它们)。

解决方案是提供一个setState mutator函数,可以在任何地方传递。

问题在于,因为我使用的是 Typescript,所以一个简单的 &lt;Component functionMutator=this.functionToSetstate 不起作用,所以我不得不更改我的 REST 层以接受一个单独的参数,即作为 Function 类型的函数。

然后将 GetServiceCommand 更改为不返回任何内容,而是在 Axios 调用完成后使用数据调用此函数,类似于 Qiarash 的建议。

以防万一有人感兴趣,这是我的更新代码:

maincomponent.js 在这里,我创建了一个 mutator 函数并将其绑定到构造函数中的“this”。然后我稍后在 ComponentDidMount() 中调用的函数中调用 REST

    setStateItems(items) 
    this.setState(
        items: items
    , function() 
        console.log("setStateItems called")
    )


    constructor(props) 
    super(props)
    this.state = 
        error: null,
        isLoaded: false,
        items: [],
    
    this.setStateItems = this.setStateItems.bind(this)


    retrieveCONFIGParameters() 
    const serviceCommand = new GetServiceCommand()
    const invoker = new ServiceInvoker(serviceCommand, EdiUrls.GET_CONFIG_PARAMETERS, this.setStateItems)
    invoker.ExecuteRequest()

GetServiceCommand.ts 您会注意到,现在只需调用从 maincomponent.js 传入的函数,而不是 Return

export class GetServiceCommand implements IServiceCommand 
execute(serviceURL: string, functionToCall: Function) 
    let returnData: Array<string>;
    returnData = []
    Axios.get(serviceURL).then(results => 
        console.log(results.data)
        results.data.forEach((element: any) => 
            console.log("test4")
            returnData.push(element)
        );
        functionToCall(returnData)
    )

再次感谢您的帮助和建议。我不相信这是解决这个问题的好方法,但我确实学到了很多关于 React、状态和 Typescript 的知识。

【问题讨论】:

ServiceInvoker 来自哪里?它是包的一部分还是您的自定义方法?通常invoker.ExecuteRequest() 应该是一个异步调用,完成后它应该使用resultData 调用setState。您当前的代码无法以这种方式运行。 amaj:感谢您的评论。 ServiceInvoker 是我创建的一个自定义类,它构成了命令设计模式的一部分。我现在正在尝试您的建议 - 不满意,但会坚持下去,稍后再报告。 @Lovelocke,这个答案可能会对你有所帮助。可以试一次吗? ***.com/a/51532619/8579855 @Lovelocke,也请参考此链接,***.com/a/42602520/8579855 【参考方案1】:

按照这些步骤操作。

    componentWillMount()调用axios 在你的状态下添加一个属性isLoading: true 当获取完成后,只需更改isLoading: false

逻辑是: if (isLoading) ? 'Loading' : render()

【讨论】:

感谢 Kamalesh - componentWillMount() 已被弃用,所以我有点犹豫要不要使用它。 @Lovelocke @kamalesh , componentDidMount 是发出 API 请求的更好地方。请参阅此处的文档。 reactjs.org/docs/react-component.html#componentdidmount componentWillMount() -> render() -> componentDidMount() 在 axios 得到响应的同时加载 true 然后 false 然后存储在 state 中是一个完美的解决方案。 1 或 2 秒的加载时间让 axios 有时间更新状态。我通过这样做解决了我的问题。【参考方案2】:

问题是 axios 调用不是同步的,这意味着在执行 axios.get 之后编译器会转到下一行:

console.log(this.returnData)
return this.returnData // [] empty array

所以为了得到正确的结果,请确保在收到来自 axios 的响应时返回:

export class GetServiceCommand implements IServiceCommand 
returnData = [] as any
execute(serviceURL: string): Object 
    // return the promise itself
    return Axios.get(serviceURL).then(results => 
        console.log(results.data)
        results.data.forEach((element: any) => 
            this.returnData.push(element)
        );
        // move return to here
        return this.returnData
    )

【讨论】:

是的。编译器转到下一行,因此它会在收到结果之前转到 this.setState 行。 @Qiarash 感谢您的回复 Qiarash - 我已经做出了您建议的更改,但现在在我的主要组件中它实际上根本没有收到任何数据。最初是 - 我明白你关于它不是同步的观点,但在 console.logs 中我可以看到有数据正在传递。 GetServiceCommand 中的 Console.logs 仍然显示它已收到数据,但返回并不愉快。 查看我的编辑,here 中还有更多关于 Promises 的信息【参考方案3】:
async retrieveCONFIGParameters() 
    const serviceCommand = new GetServiceCommand()
    const invoker = new ServiceInvoker(serviceCommand, URLS.GET_CONFIG_PARAMETERS)
    const returnData = await invoker.ExecuteRequest()

    this.setState(
        items: returnData
    , function() 
        console.log(this.state.items)
    );  

    console.log(this.state.items)



export class GetServiceCommand implements IServiceCommand 
returnData = [] as any
execute(serviceURL: string): Object 
    return Axios.get(serviceURL).then(results => 
        results.data.forEach((element: any) => 
            this.returnData.push(element)
        );
        return this.returnData;
    )

【讨论】:

@Lovelocke,这未经测试,但请试一试。 谢谢亚历山大。您的建议类似于 Qiarash 的建议,但不幸的是不起作用 - GetServiceCommand 不返回任何内容,而我的方式它不返回任何内容,只是不幸的是不会触发重新渲染。

以上是关于React - 当 Axios 在单独的组件中调用时,setState 不会重新渲染页面的主要内容,如果未能解决你的问题,请参考以下文章

React 中的 Axios AJAX 调用仅返回初始状态

React - Axios 调用发出过多请求

React - onClick 用于动态生成的组件

React + Redux + Axios 在创建动作后不加载组件

在 React Hooks useEffect cleanup 中取消 Axios REST 调用失败

React:如何将我的 ajax api 调用移动到一个单独的组件中?