How to make React Redux async action return a promise?

Tags: , , , ,



I have a redux action function that returns a promise. Inside that function body, there is another async function that returns a callback when it finished the call. Then I call this function in another place chaining it with .then() but when debugging it in the browser with breakpoints, the promise function exists after the first two lines of code

return new Promise((resolve, reject) => {
    return (dispatch, getState) => {

is it because of the second return statement? This is the react/redux code so I must have a second return statement for my async Thunk redux action. How can I achieve this? So that I could call it in another redux action and chain it with handleProfileImageUploadToS3().then(() => {...}) ?

Full function body:

export const handleProfileImageUploadToS3 = () => {
  return new Promise((resolve, reject) => {
    return (dispatch, getState) => {
      const settingsState = getState().BusinessSettings
      const userState = getState().User
      const imageUpload = true

      if (!settingsState.get('logoImage') || settingsState.get('logoImage') === null) {
        reject('no image selected')
        return
      }
      Utilities.uploadFileToS3(
        imageUpload,
        settingsState.get('logoImage'),
        'apiurl',
        `profiles/${userState.get('id')}`,
        (error, url) => {
          if (error) {
            dispatch(uploadProfileSettingsImageError(error))
            return
          }
          dispatch(updateProfileImageUrlAfterUpload(url))
          resolve(url)
        }
      )
    }
  })
}

Answer

I’m not seeing your whole codebase but there are some red flags in this function that I’m concerned about.

  • The BusinessSettings and User properties of your state seem to be non-serializable objects with a get() method.
  • If the S3 upload gives an error then you dispatch and return but you never resolve or reject the Promise.
  • The rejection from reject('no image selected') is unlikely to be caught anywhere.
  • It seems like an un-uploaded image should be something that is stored in your UI state and passed as an argument rather than stored in Redux. You would want to store the URL after it’s uploaded. This also removes the need for that rejection.

You are addressing two separate concerns in this function and I would recommend that you separate the two.

First is that you have a function Utilities.uploadFileToS3 which uses a success/failure callback and you want to convert it to an async function (one that returns a Promise).

I would make a helper that takes just the arguments which vary and ignores those that are constant (like 'apiurl').

const asyncUploadFileToS3 = (image, userId) => {
  return new Promise((resolve, reject) => {
    Utilities.uploadFileToS3(
      true,
      image,
      "apiurl",
      `profiles/${userId}`,
      (error, url) => (url ? resolve(url) : reject(error))
    );
  });
};

Now that you have that part settled, you can approach writing the thunk in a more typical way. You can return a Promise by .then() chaining or by making the function async and using a try/catch block. By we don’t need to wrap the whole thing in a new Promise because we’ve dealt with that on the asyncUploadFileToS3 function.

You can return a result from the thunk and chain it, but I’m not sure what actually makes the most sense here.

export const handleProfileImageUploadToS3 = (image) => 
  async ( dispatch, getState ) => {
    const userId = getState().user.id;
    try {
      const url = await asyncUploadFileToS3(image, userId);
      dispatch(updateProfileImageUrlAfterUpload(url));
      return "this is the result";
    } catch (error) {
      dispatch(uploadProfileSettingsImageError(error));
      return "there was an error";
    }
  };
export default function App() {
  const dispatch = useDispatch();

  const avatar = useSelector((state) => state.user.imageUrl);

  const onClick = () => {
    const image = new Blob();
    dispatch(handleProfileImageUploadToS3(image)).then(
      // logs "this is the result" or "there was an error"
      (returned) => console.log("finished", returned)
    );
  };

  return (
    <div>
      <button onClick={onClick}>Upload</button>
      {avatar ? <div>Avatar URL: {avatar}</div> : <div>No Avatar</div>}
    </div>
  );
}

Code Sandbox Demo



Source: stackoverflow