javascript - Really need some help figuring out the logic of componentWillMount() prior to render - Stack Overflow

this might be kind of long read, I've read and tried so many solutions without success! Essentiall

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?

Share Improve this question edited Aug 27, 2018 at 18:14 acd37 asked Aug 23, 2018 at 21:28 acd37acd37 5821 gold badge8 silver badges15 bronze badges 9
  • 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
 |  Show 4 more ments

3 Answers 3

Reset to default 3

Since 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

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745324576a4622595.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信