What's causing this async function to work only after a page refresh?

Viewed 852

I've been attempting to find discussions about this for over a week now, but most issues seem related to trouble persisting through a refresh, while I'm having state troubles without refreshing, so I'm not getting much of anywhere with it.

I'm attempting to load a gallery of images after a user logs in. The login is functioning properly--updates the state with a reducer and pushes from /login to /gallery and I can see in the inspector that the user ID updates from null to a value.

At /gallery I attempt to retrieve some data through axios asynchronously. It's a POST request so that I can send the user's ID in the body rather than the url/using params.

On initial login, state.images doesn't update and throws this error: "data: "Cast to string failed for value "{ user: '60fc726d827a4e3daff47619' }" (type Object) at path "user" for model "Upload"" " Relative to my database/models: I've tried adjusting type on the model, both the $type approach and adjusting for a String that is an array, the former caused errors large enough for the page not to load, the latter affected no discernible changes.

If I reload the page, everything works and the images load. If I click from gallery to home and then back to gallery again, nothing changes in the state. I have no idea if this is an issue with my amateur async function structure, my mongodb setup, my reducer, the axios post itself, or something else entirely.

I've read that pretty much everything needs to be lined up inside of useEffect() but I've had absolutely no luck getting that to function either.

The whole of the code is at https://github.com/polysnacktyl/react-foraging, but here's the (seemingly) most relevant:

Login

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { getLoggedIn } = useContext(AuthContext);
  const { dispatch } = useContext(Context);
  const history = useHistory();

  async function login(e) {
    e.preventDefault();
    try {
      const { data } = await axios.post('http://localhost:3000/auth/login', {
        email,
        password
      });

      await getLoggedIn();

      dispatch({
        type: 'login',
        payload: { user: data._id }
      })

      window.localStorage.setItem('user', JSON.stringify(data._id))

      history.push('/gallery');

    } catch (err) {
      console.error(err);
    }
  }

  return (... and so forth

Image Gallery

    const { state, dispatch } = useContext(Context);
    const [isLoading, setLoading] = useState(true);
    const [images, setImages] = useState({ images: [] });
    const user = state.user;

    const success = async () => {
        try {
            const res = await axios.post('http://localhost:3000/auth/mine', { user });

            dispatch({
                type: 'fetchSuccess',
                payload: { images: res.data }
            })

            setImages(res.data);

        } catch (err) { console.log(err.response) }
    }

    const fail = (error) =>
        dispatch({
            type: 'fetchFail',
            payload: { error: error.message }
        });

    function loadImages() {
        dispatch({ type: 'fetchImages' });
        setTimeout(async () => {
            try {
                await success();
                setLoading(false)
            } catch (error) {
                await fail(error);
            }
        }, 1000);
    }

    useEffect(() => {
        loadImages()
        //eslint-disable-next-line
    }, [])

    if (isLoading) {
        return (<div className='loading'>...loading</div>)
    } else {

        return (...and so on
2 Answers

Let's have a look at your loadImages. This function is called during componentDidMount. This seems OK on first glance but it is a bug actually.

loadImages internally calls success that actually depends on user variable.

Having this in mind what we can do is following:

useEffect(() => {
    if(!user) { // if no user present somehow, let's return
       return;
    }
    loadImages() // load the images
    //eslint-disable-next-line
}, [user]) // add a dependency to the user, since we load user images actually

I think with this approach the issue will be fixed and also you can remove the setTimeout in loadImages in my opinion.

As it turns out, my issue was in my axios-post-with-req-body strategy. I switched it to a GET request and sent the user ID in the params and now everything is loading on initial login and persisting through page relaods. I'm still not sure why it was causing the behavior it was, but at least I know how to avoid it.

the setup that ultimately worked out for me:

function Gallery() {
    const { state, dispatch } = useContext(Context);
    const [isLoading, setLoading] = useState(true);
    const [images, setImages] = useState([]);
    const user = state.user;

    const success = async () => {

        const res = await axios.get('http://localhost:3000/auth/mine', {
            params: { user }
        })
        dispatch({
            type: 'fetchSuccess',
            payload: res.data
        })

        setImages(res.data);
    }


    const fail = (error) =>
        dispatch({
            type: 'fetchFail',
            payload: { error: error.message }
        });

    function loadImages() {
        dispatch({ type: 'fetchImages' });
        setTimeout(async () => {
            try {
                await success();
                setLoading(false)
            } catch (error) {
                await fail(error);
            }
        }, 0);
    }

    useEffect(() => {
        loadImages()
        //eslint-disable-next-line
    }, [])

    if (isLoading) {
        return (<div className='loading'>...loading</div>)
    } else {

        return (...images that actually load. wonderful.)

Additionally, I had to adjust the router to accept req.params.user instead of req.body.user.

Related