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.