0
\$\begingroup\$

I have a settings section to my UI which keeps track of a myriad of user settings. The settings are requested from an API when the app loads, and then used in the section to populate what the user's current settings are. I am using redux for this. For example:

const Settings () => {
  const dispatch = useDispatch();
  const userSettings = useSelector(state => state.user.settings);
  const { getNotifications } = userSettings;

  return (
    <form>
      <label>Receive Notifications?</label>
      <input 
        type="checkbox" 
        checked={getNotifications}
        onClick={() => {
          dispatch(
            putNewUserSettings({ getNotifications: !getNotifications })
          )
        }}
      />
    </form>
  )
}

The putNewUserSettings function registers a new action in redux, which makes an api call to update the settings from the back end. When that api call returns, redux captures that, and updates state.user.settings, and the checkbox changes from being checked to unchecked, or vice versa. Great.

This, however, causes a delay in the UI. The user clicks the checkbox, but it doesn't actually change until the api call returns and updates the redux store. This makes for a sluggish-feeling UI. Not good!

My solution to this is to actually maintain the state of the checkbox (or whatever other form field) in local state, and when that changes, create an effect to make the api call:

const Settings () => {
  const dispatch = useDispatch();
  const userSettings = useSelector(state => state.user.settings);
  const { getNotifications } = userSettings;

  // Doubling of state, use redux value as initial value
  const [getNotificationsLocal, setGetNotificationsLocal] = useState(getNotifications);

  /**
   * What local state changes, make call to api so that db is updated with
   * new user settings
   */
  useEffect(() => {
    dispatch(
      putNewUserSettings({ getNotifications: getNotificationsLocal })
    )
  }, [getNotificationsLocal])

  return (
    <form>
      <label>Receive Notifications?</label>
      <input 
        type="checkbox" 
        checked={getNotificationsLocal}
        onClick={() => { setGetNotificationsLocal(!getNotificationsLocal) }}
      />
    </form>
  )
}

So in this second case, because the checkbox is reading from local state, when the user taps the checkbox, it immediately updates in the UI, and it has that performant feel. As a secondary effect, that change triggers a call to the API to 'officially' update the setting in a more global sense. (I do make use of a handy hook called useDidMountEffect to avoid running the API call on mount, as it would be unecessary to call the API to set the settings value to what it already is.)

Is this bad practice? I find myself doing this all over in an app because the api is a touch slow and causing for slow UI. Is this some kind of anti-pattern that will cause more problems I'm not foreseeing?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

What you've done makes sense to me; I see this approach all the time for autosaving. I don't yet know of a library that eases this kind of manual pain.

However, here are some caveats with your current approach:

  1. The user could toggle the checkbox in quick succession, perhaps from an accidental double click. This opens up a potential race condition with the API calls: the sending order may not equal the fulfillment order (from the BE). Due to this, the final state in Redux may become out of sync with your local state. And the user may feel distrust through this bad experience. My normal solution to this is to track the loading state of the API request inflight, and disable the checkbox until it's done. This prevents the opportunity of multiple inflight requests, and ensures that the UI stays in sync.
  2. What happens if the save request fails? You've optimistically updated your local state. But now there's a mismatch between the local and BE state. My normal solution for this, inconjunction with point 1, would be to show an error message nearby and revert the local state to what it was before.

Re. 1, and this is more subjective: Perhaps introduce a bit of loading UI near the checkbox, like a spinner, to indicate the save is in progress. As a user, that'll clearly indicate an autosave is happening, which is an immediate response to their interaction, meeting your perceived speed requirements, but also provides the confidence of the save being performed.

\$\endgroup\$
2
  • \$\begingroup\$ I had thought about the scenario where the api call fails, I just didn't include it in the question as the question was getting long. I didn't think about what happens if the user clicks quickly in succession. Thanks for the insight! \$\endgroup\$ Mar 29, 2022 at 14:06
  • \$\begingroup\$ @SethLutske Yeah, that makes sense. You're welcome - good luck with your project. \$\endgroup\$
    – pjlangley
    Mar 29, 2022 at 14:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.