this might be kind of long read, I've read and tried so many solutions without success! Essentially what I have is three MySQL tables, one with a list of users, and one with a list of file data. They are paired with a third table, which has a column for user id and a column for file id.
When a user logs into the app, it grabs their ID from Table 1, goes to Table 3, finds all the file IDs that are in the same row as their user ID, and then returns the file information from Table 2. Mostly straight forward, except it's not.
My current code:
ponentWillMount() {
this.getClientFiles();
}
Which calls:
getClientFiles() {
let id = this.props.currentUser.user_id;
let file_refs = [];
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.setState({
file_refs
});
this.getFileData();
});
}
My understanding of this is that this.getFileData();
should ONLY run once the axios GET request is successful (because of .then
). The file refs are all returned, and the added to an array and put in state for the duration of the client's session.
Then this should run:
getFileData() {
let fileRefs = this.state.file_refs;
let fileData = [];
for (let i = 0; i < fileRefs.length; i++) {
axios
.get("/files/get-file/" + fileRefs[i])
.then(res => {
fileData.push(res.data.response);
this.setState({
client_files: fileData,
returned_data: true
});
})
.catch(err => console.log(err.response.data));
}
}
Here, the function cycles through the fileRefs in state, makes a call for each reference ID, and returns that to fileData and saves it to state.
The problem.... on first page load after a login, the files do not render. If you hit cmd+R to refresh, boom there they are. I understand the chain of promises, and the async nature of JS functions, I understand that ponentWillMount()
should run prior to the mounting of the ponent, and that setState
should trigger a re-render of a ponent.
Things I've tried:
1) Adding the following code in after render()
prior to return(
:
if (this.state.returned_data === false) {
this.getClientFiles();
}
The result is a flickering of renders, 4-5 of them, as the functions run async before the state of returned_data is set to true.
2) Moving the setState({ returned_data: true })
into the getClientFiles()
function. This just ends the render early, resulting in no files until the page is refreshed.
3) Swapping out ponentWillMount()
for ponentDidMount()
.
Clearly, there is a fundamental aspect of the chain of functions and React's built in methods that I'm missing.
Can anybody help?
EDIT #1
The issue seems to be that on first render, let id = this.props.currentUser.user_id;
is undefined
, so the call in getClientFiles
is actually going to /users/get-client-files/undefined
EDIT #2 - Requested by @devserkan I hope this is what you wanted :)
First load
get-client-files/${id}
: Returns an empty array
/get-file/" + fileRefs[i]
: Doesn't run
Second load:
get-client-files/${id}
: Returns array with 5 items
/get-file/" + fileRefs[i]
: Runs appropriately 5 times with the details of each file.
So clearly, the issue is with the fact that get-client-files/${id}
isn't getting anything because it doesn't have the ${id}
to search from. The ID is passed down via props, but doesn't seem to be available immediately.
EDIT #3 Here is the function that gets the ID, and sets it to state.
getUser = () => {
let localToken = localStorage.getItem("iod_tkn");
axios({
url: "/admins/current",
method: "get",
headers: {
Authorization: localToken
}
})
.then(result => {
this.setState({
isLoggedIn: true,
user: result.data,
user_id: result.data.user_id
});
})
.catch(err => {
this.setState({ isLoggedIn: false });
console.log(err);
});
};
And App.js renders the following:
render() {
const { loading } = this.state;
if (loading) {
return <Spinner />;
}
return (
<AdminProvider>
<FileProvider>
<Provider>
<Appbar isLoggedIn={this.state.isLoggedIn} logout={this.logout} />
<Main
getUser={this.getUser}
isLoggedIn={this.state.isLoggedIn}
currentUser={this.state.user}
/>
<BottomNav />
</Provider>
</FileProvider>
</AdminProvider>
);
}
So with passing this.state.user
into Main.js, that ponent should re-render once the props have been received, right?
this might be kind of long read, I've read and tried so many solutions without success! Essentially what I have is three MySQL tables, one with a list of users, and one with a list of file data. They are paired with a third table, which has a column for user id and a column for file id.
When a user logs into the app, it grabs their ID from Table 1, goes to Table 3, finds all the file IDs that are in the same row as their user ID, and then returns the file information from Table 2. Mostly straight forward, except it's not.
My current code:
ponentWillMount() {
this.getClientFiles();
}
Which calls:
getClientFiles() {
let id = this.props.currentUser.user_id;
let file_refs = [];
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.setState({
file_refs
});
this.getFileData();
});
}
My understanding of this is that this.getFileData();
should ONLY run once the axios GET request is successful (because of .then
). The file refs are all returned, and the added to an array and put in state for the duration of the client's session.
Then this should run:
getFileData() {
let fileRefs = this.state.file_refs;
let fileData = [];
for (let i = 0; i < fileRefs.length; i++) {
axios
.get("/files/get-file/" + fileRefs[i])
.then(res => {
fileData.push(res.data.response);
this.setState({
client_files: fileData,
returned_data: true
});
})
.catch(err => console.log(err.response.data));
}
}
Here, the function cycles through the fileRefs in state, makes a call for each reference ID, and returns that to fileData and saves it to state.
The problem.... on first page load after a login, the files do not render. If you hit cmd+R to refresh, boom there they are. I understand the chain of promises, and the async nature of JS functions, I understand that ponentWillMount()
should run prior to the mounting of the ponent, and that setState
should trigger a re-render of a ponent.
Things I've tried:
1) Adding the following code in after render()
prior to return(
:
if (this.state.returned_data === false) {
this.getClientFiles();
}
The result is a flickering of renders, 4-5 of them, as the functions run async before the state of returned_data is set to true.
2) Moving the setState({ returned_data: true })
into the getClientFiles()
function. This just ends the render early, resulting in no files until the page is refreshed.
3) Swapping out ponentWillMount()
for ponentDidMount()
.
Clearly, there is a fundamental aspect of the chain of functions and React's built in methods that I'm missing.
Can anybody help?
EDIT #1
The issue seems to be that on first render, let id = this.props.currentUser.user_id;
is undefined
, so the call in getClientFiles
is actually going to /users/get-client-files/undefined
EDIT #2 - Requested by @devserkan I hope this is what you wanted :)
First load
get-client-files/${id}
: Returns an empty array
/get-file/" + fileRefs[i]
: Doesn't run
Second load:
get-client-files/${id}
: Returns array with 5 items
/get-file/" + fileRefs[i]
: Runs appropriately 5 times with the details of each file.
So clearly, the issue is with the fact that get-client-files/${id}
isn't getting anything because it doesn't have the ${id}
to search from. The ID is passed down via props, but doesn't seem to be available immediately.
EDIT #3 Here is the function that gets the ID, and sets it to state.
getUser = () => {
let localToken = localStorage.getItem("iod_tkn");
axios({
url: "/admins/current",
method: "get",
headers: {
Authorization: localToken
}
})
.then(result => {
this.setState({
isLoggedIn: true,
user: result.data,
user_id: result.data.user_id
});
})
.catch(err => {
this.setState({ isLoggedIn: false });
console.log(err);
});
};
And App.js renders the following:
render() {
const { loading } = this.state;
if (loading) {
return <Spinner />;
}
return (
<AdminProvider>
<FileProvider>
<Provider>
<Appbar isLoggedIn={this.state.isLoggedIn} logout={this.logout} />
<Main
getUser={this.getUser}
isLoggedIn={this.state.isLoggedIn}
currentUser={this.state.user}
/>
<BottomNav />
</Provider>
</FileProvider>
</AdminProvider>
);
}
So with passing this.state.user
into Main.js, that ponent should re-render once the props have been received, right?
- If you make this a CodeSandbox, I'll fix it for you. – Colin Ricardo Commented Aug 23, 2018 at 22:52
- I'll see what I can about getting it into a sandbox tomorrow, not sure how to go about that as it has so much tied into the code that I didn't post here (db etc.) Ideally, I'm looking to understand WHY what I'm doing isn't working, though. – acd37 Commented Aug 24, 2018 at 0:24
-
So, in the first place, if the
user_id
is undefined, you need to check other parts of your ponent. – devserkan Commented Aug 24, 2018 at 22:13 - Right but it returns the appropriate id after the page refreshes.. that's what I can't figure out. – acd37 Commented Aug 24, 2018 at 22:17
- Page refresh? Weird. How do you get this prop? – devserkan Commented Aug 24, 2018 at 22:32
3 Answers
Reset to default 3Since your user_id
is ing from an async job, you should do a conditional rendering. Like:
{ user_id && <ClientDashboard user_id={user_id} ... /> }
Also, you can clean up your code a little bit more maybe :) Here I am mimicking your app.
const userFiles = [
{ file_id: 1, client_name: "foo" },
{ file_id: 2, client_name: "bar" },
{ file_id: 3, client_name: "baz" },
];
const files = [
{ file_id: 1, name: "fizz", size: 10 },
{ file_id: 2, name: "buzz", size: 20 },
{ file_id: 3, name: "fuzz", size: 30 },
];
const fakeRequest = () => new Promise( resolve =>
setTimeout( () => resolve(userFiles), 1000)
);
const fakeRequest2 = id => new Promise(resolve => {
const file = files.find( el => id === el.file_id );
setTimeout(() => resolve(file), 1000)
}
);
class App extends React.Component {
state = {
file_refs: [],
client_files: [],
returned_data: false,
}
ponentDidMount() {
this.getClientFiles();
}
getClientFiles() {
fakeRequest()
.then(res => {
const file_refs = res.map( el => el.file_id );
this.setState({
file_refs
});
this.getFileData();
});
}
getFileData() {
const {file_refs: fileRefs} = this.state;
const promiseArray = fileRefs.map( id => fakeRequest2( id ) );
Promise.all( promiseArray )
.then( results => this.setState({
client_files: results,
returned_data: true,
}))
}
render() {
const { file_refs, client_files } = this.state;
return (
<div>
{!!file_refs.length && <p>File_refs: {JSON.stringify(file_refs)}</p>}
{!!client_files.length && <p>Client files: {JSON.stringify(client_files)}</p>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare./ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I don't like for loops :)
The problem is that in ponentWillMount()
an async call might not retrieve the results on time before the render of the mount phase happens, so you will have unexpected side effects. Most probably the ponent will render with empty data.
The best place to render data from an async call is ponentDidMount()
.
As a side note, from 16.3 version on, ponentWillMount()
is considered an unsafe method of the lifecycle, and in future versions will be removed, so you better not use it anymore.
I think there's an issue with your code structuring. setState
is an async function which takes a callback as a second parameter. You should take its advantage. You can execute a function after setState is finishing and utilize updated state using the second param callback (updater function) like:
this.setState({
file_refs
}, () => {
this.getFileData();
});
EDITED Second option you shouldn't setState file_refs
unless you're using it in your render method.
Try this:
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.getFileData(file_refs);
});
getFileData(file_refs) {
let fileRefs = file_refs;
let fileData = [];
// rest of your code
}
Let me know if the issue still persists. Happy to help