Search code examples
reactjsaxiosredux-sagaredux-thunk

How to queue requests using react/redux?


I have to pretty weird case to handle.

We have to few boxes, We can call some action on every box. When We click the button inside the box, we call some endpoint on the server (using axios). Response from the server return new updated information (about all boxes, not the only one on which we call the action).

Issue: If user click submit button on many boxes really fast, the request call the endpoints one by one. It's sometimes causes errors, because it's calculated on the server in the wrong order (status of group of boxes depends of single box status). I know it's maybe more backend issue, but I have to try fix this on frontend.

Proposal fix: In my opinion in this case the easiest fix is disable every submit button if any request in progress. This solution unfortunately is very slow, head of the project rejected this proposition.

What we want to goal: In some way We want to queue the requests without disable every button. Perfect solution for me at this moment:

  • click first button - call endpoint, request pending on the server.
  • click second button - button show spinner/loading information without calling endpoint.
  • server get us response for the first click, only then we really call the second request.

I think something like this is huge antipattern, but I don't set the rules. ;)

I was reading about e.g. redux-observable, but if I don't have to I don't want to use other middleware for redux (now We use redux-thunk). Redux-saga it will be ok, but unfortunately I don't know this tool. I prepare simple codesandbox example (I added timeouts in redux actions for easier testing).

I have only one stupid proposal solution. Creating a array of data needs to send correct request, and inside useEffect checking if the array length is equal to 1. Something like this:

const App = ({ boxActions, inProgress, ended }) => {
  const [queue, setQueue] = useState([]);

  const handleSubmit = async () => {  // this code do not work correctly, only show my what I was thinking about 

    if (queue.length === 1) {
      const [data] = queue;
      await boxActions.submit(data.id, data.timeout);
      setQueue(queue.filter((item) => item.id !== data.id));
  };
  useEffect(() => {
    handleSubmit();
  }, [queue])


  return (
    <>
      <div>
        {config.map((item) => (
          <Box
            key={item.id}
            id={item.id}
            timeout={item.timeout}
            handleSubmit={(id, timeout) => setQueue([...queue, {id, timeout}])}
            inProgress={inProgress.includes(item.id)}
            ended={ended.includes(item.id)}
          />
        ))}
      </div>
    </>
  );
};

Any ideas?


Solution

  • In case you would like to use redux-saga, you can use the actionChannel effect in combination with the blocking call effect to achieve your goal:

    Working fork: https://codesandbox.io/s/hoh8n

    Here is the code for boxSagas.js:

    import {actionChannel, call, delay, put, take} from 'redux-saga/effects';
    // import axios from 'axios';
    import {submitSuccess, submitFailure} from '../actions/boxActions';
    import {SUBMIT_REQUEST} from '../types/boxTypes';
    
    function* requestSaga(action) {
      try {
        // const result = yield axios.get(`https://jsonplaceholder.typicode.com/todos`);
        yield delay(action.payload.timeout);
        yield put(submitSuccess(action.payload.id));
      } catch (error) {
        yield put(submitFailure());
      }
    }
    
    export default function* boxSaga() {
      const requestChannel = yield actionChannel(SUBMIT_REQUEST); // buffers incoming requests
      while (true) {
        const action = yield take(requestChannel); // takes a request from queue or waits for one to be added
        yield call(requestSaga, action); // starts request saga and _waits_ until it is done
      }
    }
    

    I am using the fact that the box reducer handles the SUBMIT_REQUEST actions immediately (and sets given id as pending), while the actionChannel+call handle them sequentially and so the actions trigger only one http request at a time.

    More on action channels here: https://redux-saga.js.org/docs/advanced/Channels/#using-the-actionchannel-effect