4

I'm doing some R&D in work and decided to learn React and build an application whilst doing so.

I've come across a peculiar problem and it's got me scratching my head. I couldn't find an exact match to my problem already on Stackoverflow so thought I would make a post.

The tl;dr of the problem: Making an Axios call outside of my component and then setting state will not trigger a rerender, but making the Axios call within the component does.

I have renamed some parts of the code.

Application Summary

I have created the frontend part of an MVC application, which will talk to my colleague's Spring Boot server to retrieve "config" data, which will be retrieved from an SQL database. My frontend will display this config data and allow it to be edited.

I'm using Axios for the REST calls.

I have an Axios call in the componentDidMount() of one of my components which does an initial call to the server and then adds those results to that component's state. The page then rerenders and a table is populated with the results. The table is its own component, with each entry its own component as well. Data is passed to these components via props.

The Problem

If I keep the Axios call within the componentDidMount() then my application works as expected.

However, I have multiple components making REST calls so there's duplicated code so I wanted to essentially make a REST service within my application that any component can call. I did this by following the Command design pattern, and I even wrote it in Typescript. Overkill I know, but this is for R&D and I just wanted to do it as a learning exercise.

So to use this I have a function in my component that sets up the command, the invoker with the command and url, and then calls execute on the invoker and adds the results into a variable.

So here's the issue: if I make use of my REST service, which has the Axios call in a class outside of my component, then the rerender doesn't happen and my table isn't populated with data. But if I keep the Axios call inside my componentDidMount() then everything works fine.

What I've Tried

After playing about with lots of console.logs I saw that the setState wasn't getting the data before it set the state, so my "items" in state was blank. I resolved this by adding 'async' onto the function and 'await' onto the setState call. Now I can see via console.log that the state has the data, however the rerender is still not triggering.

I also tried moving things about - so I had the function and everything within componentDidMount(), then I moved everything out and simply called that function within componentDidMount() - this hasn't worked.

My Code

maincomponent.js This is the component that instigates the Axios call. You'll see that my original Axios call is commented out.

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 This is the GET command that has the Axios call in.

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
}

}

If necessary I can provide the code from the other classes but they're just the standard Command pattern classes so I don't think it's needed.

Final Thoughts

I know that what I've done is likely overkill, and that going back to simply using Axios calls within each component will make this problem go away. But it's been on my mind for a few days and I'd just love to know what I've done wrong.

And one final thing: the console.log for the data from the Axios call, and from the state, look different. From the Axios call itself the data looks like this:

(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

And from the state:

[]
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

You'll notice the state begins with [], whereas the Axios data is (6). I'm not sure if this is what's causing the issue... I think it's unlikely because they're just arrays, but thought I'd mention it just in case.

Thanks to anyone who takes the time to look at this.

EDIT

Thanks to everyone who offered their advice. I have now managed to resolve the problem, although it feels a bit dirty.

The suggestions highlighted the issue with the Axios call and the return - after spending some time with a ton of console.logs all over the show I came to realise that the Return in GetServiceCommand is actually blank; this is confusing because other console.logs show this to have data once it gets into the component, but this can't be right because the Return fires before the data is added to the array. I think the console.logs essentially mislead me (or, more accurately, I misunderstood them).

The solution is to provide a setState mutator function that can be passed wherever.

The problem is that because I I'm using Typescript a simple <Component functionMutator={this.functionToSetstate} won't work, so I had to change my REST layer to accept a separate parameter, that being the function as type Function.

Then change the GetServiceCommand to not return anything, but rather call this function with the data once the Axios call has finished, in a similar fashion to Qiarash's suggestion.

Just in case anyone is interested, here is my updated code:

maincomponent.js Here I've created a mutator function and bound it to 'this' in the constructor. I then have the call to the REST later within a function that is called in ComponentDidMount()

    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 You'll notice that instead of a Return there's now simply a call to the function that was passed in from maincomponent.js

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)
    })
}

}

Thanks again for your help and suggestions. I'm not convinced this is a good way of going about this, but I certainly learned a lot about React, state, and Typescript.

Lovelocke
  • 41
  • 4
  • Where `ServiceInvoker` is coming from? Is it part of a package or your custom method? In general `invoker.ExecuteRequest()` should be an async call and after it is done it should call `setState` with resultData. Your current code is not working this way. – amaj Mar 03 '20 at 09:42
  • amaj: thanks for your comment. ServiceInvoker is a custom class I created, which forms part of the Command design pattern. I'm trying your suggestion now - it's not happy, but will keep at it and report back later. – Lovelocke Mar 03 '20 at 09:48
  • @Lovelocke, This answer may help you. Can you try it once? https://stackoverflow.com/a/51532619/8579855 – Gokulakannan T Mar 03 '20 at 11:14
  • @Lovelocke, Refer this link too, https://stackoverflow.com/a/42602520/8579855 – Gokulakannan T Mar 03 '20 at 11:16

3 Answers3

2

The problem is that axios call is not synchronous meaning just after axios.get is executed the complier goes to next line which is:

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

so in order to get the correct result, make sure you return when you get response from 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
    })
}
StackedQ
  • 3,999
  • 1
  • 27
  • 41
  • Yes. the compiler goes to next line so it will go to this.setState line before receiving result. @Qiarash – Alex Mar 03 '20 at 10:22
  • Thanks for your reply Qiarash - I've made the changes you've suggested but now in my maincomponent it's actually not receiving any data at all. Originally it was - I get your point regarding it not being synchronus, but in console.logs I can see there was data being passed. Console.logs in GetServiceCommand still show it has received the data, but that return isn't happy. – Lovelocke Mar 03 '20 at 10:31
  • Check out my edit, there's also more info on Promises in [here](https://scotch.io/tutorials/javascript-promises-for-dummies) – StackedQ Mar 03 '20 at 10:33
1
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;
    })
}
Alex
  • 1,148
  • 8
  • 30
  • @Lovelocke, This is not tested but please give it a try. – Alex Mar 03 '20 at 10:32
  • Thanks Alexandr. Your suggestion is similar to Qiarash's and unfortunately doesn't work - the GetServiceCommand doesn't return anything, whereas my way it doesn't return something, just unfortunately doesn't trigger the rerender. – Lovelocke Mar 03 '20 at 10:36
1

Follow these steps.

  1. call axios from componentWillMount()
  2. add a property in ur state isLoading: true
  3. when fetching completed then just change isLoading: false

Logic is : if (isLoading) ? 'Loading' : render()

kamalesh biswas
  • 883
  • 8
  • 9
  • Thanks Kamalesh - componentWillMount() is being deprecated so I'm a little hesitant to use it. – Lovelocke Mar 03 '20 at 11:03
  • @Lovelocke @kamalesh , `componentDidMount` is a better place to make API requests. See the document here. https://reactjs.org/docs/react-component.html#componentdidmount – Gokulakannan T Mar 03 '20 at 11:10
  • componentWillMount() -> render() -> componentDidMount() – kamalesh biswas Mar 04 '20 at 05:18
  • Loading true and then false while axios getting response and then storing in state is a perfect solution. 1 or 2 second loading gives time to axios to update state. i solved my problem by doing this. – Tariq Murtuza Nov 22 '20 at 21:02