1

I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:

I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.

I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.

Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';

const Loader = (props) => {
  const pollDatabase = (jobId, pollFunction) => { 
    return delay(5000)
      .then(pollFunction(jobId))  
        .catch(err => console.log("Failed in pollDatabase function. Error: ", err))
        };

  const pollUntilComplete = (jobId, pollFunction) => { 
    return pollDatabase(jobId, pollFunction)
        .then(res => {
            console.log(props.loadJobCompletionStatus) // <- always null
            if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
                return pollUntilComplete(jobId, pollFunction);
            }
        })
        .catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
    };


  const uploadHandler = () => {
  ...
    const transferPromise = apiCall1() // Names changed to reduce code
      .then(res=> {
        return axios.post(api2url, res.data.id);
            })
      .then(postResponse=> {
        return axios.put(api3url, file)
        .then(()=>{ 
          return instance.post(api3url, postResponse.data) 
        })
      })

  transferDataPromise.then((res) => {
    return pollUntilComplete(res.data.job_id, 
      props.checkLoadTaskStatus)
        })
        .then(res => console.log("Task complete: ", res))
        .catch(err => console.log("An error occurred: ", err))
  }

return ( ...); // 

const mapStateToProps = state => {
    return {
        datasets: state.datasets,
        loadJobCompletionStatus: state.loadJobCompletionStatus,
        loadJobErrorStatus: state.loadJobErrorStatus,
        loadJobIsPolling: state.loadJobPollingFirestore
    }
}

const mapDispatchToProps = dispatch => {
  return {
    checkLoadTaskStatus: (jobId) => 
         dispatch(actions.loadTaskStatusInit(jobId))
    };
};


export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);


delay.js
export const delay = (millis) => {
    return new Promise((resolve) => setTimeout(resolve, millis));
}

actions.js
...
export const loadTaskStatusInit = (jobId) => {
  return dispatch => {
      dispatch(loadTaskStatusStart()); // 
      const docRef = firestore.collection('coll').doc(jobId) 
        return docRef.get() 
        .then(jobData=>{
            const completionStatus = jobData.data().complete;
            const errorStatus = jobData.data().error;
            dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
        },
        error => {
            dispatch(loadTaskStatusFail(error));
        })
    };
}


It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.

I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.

Can anybody point out what might be causing this?

Thanks

James Z
  • 12,209
  • 10
  • 24
  • 44
Snek
  • 59
  • 1
  • 11
  • Have you looked into redux sagas? – Matt Sugden Oct 29 '19 at 15:54
  • Yes I have used sagas. This code base was mainly written with thunk though. However, I am not so sure sagas would solve this issue. I think this is more related to trying to access an updated state in the same render. – Snek Oct 29 '19 at 19:31
  • I only asked as I've always moved any api related activity into sagas, and it's worked pretty well for me. You could use an effect to dispatch a single action (startPolling e.g.), which the saga would pick up, do the polling, dispatching actions to update state accordingly. I'd also look into the new hooks in react-redux, useSelector and useDispatch, saves you having to wrap your components with the connect hoc. – Matt Sugden Oct 29 '19 at 20:58
  • Hi @Matt thanks for that. I spent a couple hours re-reading the hooks documentation and I think I have come up with a few ways to overcome the issue. It would have been nice to put this into a Redux so that it persisted across all pages and I could dump the whole thing into local storage but I don't think my use case allows it. I agree that sagas is the way to go, especially with more complex and larger code bases in my opinion. I will be using that and/or some of the other hooks going forward for new projects. – Snek Oct 30 '19 at 10:23

1 Answers1

1

I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.

As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.

markerikson
  • 63,178
  • 10
  • 141
  • 157
  • Hi Mark, I have gone through the article. It was a a very insightful read. It definitely seems that because I am in the same render that I would not have access to the updated state, if I was using React hooks only. Do you know if the logic is the same for the Redux store? I am going to go through the useRef hook to see how it is used in this context. – Snek Oct 29 '19 at 19:30
  • No, it doesn't matter whether you're using a React hook or something Redux related - the code as written would be capturing the existing value at the time of render. You need one of the alternate approaches / escape hatches I listed to work around that. – markerikson Oct 29 '19 at 19:57
  • Thanks Mark. Makes perfect sense. I will have to bite the bullet and refactor some code. Appreciate the advice. – Snek Oct 30 '19 at 10:27