Really need some help figuring out the logic of co

2019-07-13 17:37发布

问题:

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:

componentWillMount() {
    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 componentWillMount() should run prior to the mounting of the component, and that setState should trigger a re-render of a component.

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 componentWillMount() for componentDidMount().

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 component should re-render once the props have been received, right?

回答1:

Since your user_id is coming 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,
  }

  componentDidMount() {
    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.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

I don't like for loops :)



回答2:

The problem is that in componentWillMount() 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 component will render with empty data. The best place to render data from an async call is componentDidMount().

As a side note, from 16.3 version on, componentWillMount() is considered an unsafe method of the lifecycle, and in future versions will be removed, so you better not use it anymore.



回答3:

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