title: React/redux-saga: How to periodically update data from the server that other users have made, while also making changes to it?
body: <p>I have a project where a user makes changes to a page which displays a list of tasks which have been assigned to other users. When the user makes a change it sends the data to the REST API using either POST or PUT and if redux-saga completes the request successfully it inserts that data into the working list of tasks. This works quite well when only one person is working on that data, but I'd also like the page to update itself when external changes are made.</p>
  
  <p>For example - my project is a service that dispatchers couriers on tasks. When a rider has picked up or delivered an item, I want them to use their smartphone to press a button that send the current time to the server (time of pickup/delivery). At some point I want that change to be reflected on the coordinator's view (the person at home recording and assigning deliveries).</p>
  
  <p>I understand using a web socket is often recommended for this approach. I may have done myself a disservice by not using that approach in the first place, as everything is done using REST and redux-saga. I'm imaging it would take a lot of rewriting to make it work with a socket instead, unless it is worth making a socket that somehow only retrieves changes from external sources.</p>
  
  <p>I could do that if it needs to be done. However I don't know if it's necessary, because I'm not really aiming for real-time collaboration (like say google docs or monday does). It just needs to periodically refresh itself say every 10 seconds.</p>
  
  <p>This is what I've done so far:</p>
  
  <p>All tasks have a parent "session" object in the database. All tasks for the coordinator's current shift are associated with it. I added a field when retrieving details about the session called "tasks_etag" which sends a hash of all the tasks.</p>
  
  <p>Then my page makes a periodic call to this endpoint (every 10 seconds at the moment) and if the etag changes it triggers an attempt to refresh the data.</p>
  
  <p>If the coordinator is currently waiting on a request to send data to the server, it waits and delays the refresh by a few seconds before checking again. It should only retrieve data when no data is currently being sent (I might also disable controls while the refresh request is in progress, so that the coordinator has to wait a moment before changing anything).</p>
  
  <p>I've copied I think the most relevant piece of code here for what I have so far. The full file can also be seen <a href="https://github.com/duckbytes/bloodbike/blob/master/react_app/src/containers/SessionDetail.js" rel="nofollow noreferrer">here.</a></p>
  
  <p>SessionDetail.js</p>
  
  <pre><code>const isFetching = useSelector(state =&gt; loadingSelector(state));
  const isTaskPostingSelector = createPostingSelector([
      "ADD_TASK",
      "GET_TASK",
      "DELETE_TASK",
      "RESTORE_TASK",
      "UPDATE_TASK",
      "UPDATE_TASK_CONTACT_NAME",
      "UPDATE_TASK_CONTACT_NUMBER",
      "UPDATE_TASK_PICKUP_ADDRESS",
      "UPDATE_TASK_DROPOFF_ADDRESS",
      "UPDATE_TASK_PICKUP_TIME",
      "UPDATE_TASK_DROPOFF_TIME",
      "UPDATE_TASK_CANCELLED_TIME",
      "UPDATE_TASK_REJECTED_TIME",
      "UPDATE_TASK_ASSIGNED_RIDER",
      "UPDATE_TASK_PRIORITY",
      "UPDATE_TASK_PATCH"
  ]);
  const isTaskPosting = useSelector(state =&gt; isTaskPostingSelector(state));
  const tasks = useSelector(state =&gt; state.tasks.tasks);
  const viewMode = useSelector(state =&gt; state.viewMode);
  const mobileView = useSelector(state =&gt; state.mobileView);
  const notFoundSelector = createNotFoundSelector(["GET_SESSION"]);
  const notFound = useSelector(state =&gt; notFoundSelector(state));
  //TODO: This could put data into title
  const currentSession = useSelector(state =&gt; state.currentSession.session);
  const tasksEtag = useSelector(state =&gt; state.currentSession.session.tasks_etag);
  const session_uuid = props.match ? decodeUUID(props.match.params.session_uuid_b62) : currentSession.uuid;
  const history = useHistory();
  const firstUpdate = useRef(true);
  
  const [rightSideBarOpen, setRightSideBarOpen] = useState(true);
  
  function componentDidMount() {
      dispatch(getAllTasks(session_uuid));
      dispatch(getSession(session_uuid))
      dispatch(setCommentsObjectUUID(session_uuid));
      if (!viewMode) {
          const viewModeLocalStorage = getLocalStorageViewMode();
          if (viewModeLocalStorage === null)
              dispatch(setViewMode("kanban"));
          else
              dispatch(setViewMode(viewModeLocalStorage));
      }
      if (!props.match) {
          history.push(`/session/${encodeUUID(currentSession.uuid)}`);
      }
  }
  
  useEffect(componentDidMount, []);
  
  function setupRefreshTimer() {
      const timer = setInterval(() =&gt; {
          dispatch(refreshCurrentSession(session_uuid));
      }, 10000)
      return function cleanup() {
          clearInterval(timer);
      };
  }
  useEffect(setupRefreshTimer, []);
  
  let deferredRefresh = undefined;
  
  function checkRefresh() {
      console.log("checkRefresh running")
      if (!isTaskPosting) {
          console.log("not posting so going to refresh")
          dispatch(refreshAllTasks(session_uuid));
          clearTimeout(deferredRefresh)
          deferredRefresh = undefined;
      } else {
          console.log("is posting so gonna wait")
          clearTimeout(deferredRefresh)
          deferredRefresh = undefined;
          deferredRefresh = setTimeout(checkRefresh, 1000)
      }
  }
  
  function refreshData() {
      // We don't need it to run the first time
      if (firstUpdate.current) {
          console.log("First time so ignore")
          firstUpdate.current = false;
      } else {
          // If a loop is already waiting to refresh, ignore
          if (!deferredRefresh) {
              console.log("The refresh loop is not waiting")
              checkRefresh();
          }
          else {
              console.log("The refresh loop is already going")
          }
      }
  }
  useEffect(refreshData, [tasksEtag])
  </code></pre>
  
  <p>createPostingSelector</p>
  
  <pre><code>export const createPostingSelector = actions =&gt; state =&gt; {
      return actions.some(action =&gt; state.postingReducer[action]);
  };
  </code></pre>
  
  <p>postingReducer:</p>
  
  <pre><code>function postingReducer(state = {}, action) {
      const {type} = action;
      const matches = /(.*)_(REQUEST|SUCCESS|FAILURE|NOTFOUND)/.exec(type);
  
      // not a *_REQUEST / *_SUCCESS /  *_FAILURE actions, so we ignore them
      if (!matches) return state;
  
      const [, requestName, requestState] = matches;
      return {
          ...state,
          // Store whether a request is happening at the moment or not
          [requestName]: requestState === 'REQUEST',
      };
  }
  </code></pre>
  
  <p>One problem with this approach is that I can't know where changes have come from, so when the coordinator makes a change to a task, it will inevitably come back with a changed etag and then pointlessly refresh everything. I want to know if there is some way I can avoid this happening, but I can't think of a good solution except maybe trying to calculate the hash on the client side at the same time. This seems expensive if there are a lot of tasks to hash and probably not reliable as some data that exists on the server (but isn't essential for operation) does not get inserted into the client side state.</p>
  
  <p>I also find that after sending lots of data to the server, I end up with multiple loops that are waiting for isTaskPosting to be false, but it never resolves, so my console gets spammed with messages that say it needs to do a check but isn't going to.</p>
  
  <p>Does anyone have advice about if I am approaching this problem in the right way? Should I be rethinking how I'm going to do this? Are there better ways or some differences I should be making?</p>
  
  <p>Thanks very much.</p>