lostpebble / pullstate

Simple state stores using immer and React hooks - re-use parts of your state by pulling it anywhere you like!
https://lostpebble.github.io/pullstate
MIT License
1.08k stars 23 forks source link

[Question] Update store not working in async function? #74

Closed Je12emy closed 3 years ago

Je12emy commented 3 years ago

Sorry if this is not the right place to post this, but I've been trying to implement Pullstate into my RN application in order to reduce the amount of fetch request done throughout the app. For now, I'm fetching some basic data like the user's profile and saving it into the store.

In a "View Profile" screen I'm updating the user's profile photo with expo's image picker and it seems the property is being mutated since the new profile image is rendered as expected (there's a header component which render the profile image too) but when throwing a put request to update the profile, it seems the state before the update to the store was done, is being sent (So the original data which was fetched).

I noticed that after doing the same operation (picking a new profile image) does indeed send the previous intended data too.

  // TEST FOR GLOBAL STATE
  const userProfile = UIProfile.useState();

 const updateProfile = async () => {
  // GET THE AUTH TOKEN
  const token = await SecureStore.getItemAsync(SID_KEY);
  if (userProfile) {
    // DESTRUCTURE NEEDED DATA
    const { foto_de_perfil: profileImage, tags } = userProfile;
    // PUT REQUEST
    await onUpdate(
      { authToken: token as string },
      {
        profileImage,
        tags,
      }
    );
  }
};

const updateProfileImage = async (pickerResult: string) => {
  // UPDATE DOES NOT OCCUR ON STORE WITH THE PICKER RESULT URI
  UIProfile.update((p) => {
    if (p) {
      p.foto_de_perfil = pickerResult as string;
    }
  });

  await updateProfile();
};

// Change profile image
const openImagePickerAsync = async () => {
  const permissionResult = await ImagePicker.requestCameraRollPermissionsAsync();

  if (permissionResult.granted === false) {
    alert("Permission to access camera roll is required!");
    return;
  }

  const pickerResult = await ImagePicker.launchImageLibraryAsync({
    allowsEditing: true,
  });

  if (pickerResult.cancelled === true) {
    return;
  }
  // UPDATE THE STORE STATE
  await updateProfileImage(pickerResult.uri);
};
lostpebble commented 3 years ago

Hi @Je12emy ,

I'm not entirely sure what is going on here, and in order to help you I might need more context.

Where is the variable userProfile coming from here?

const updateProfile = async () => {
  // GET THE AUTH TOKEN
  const token = await SecureStore.getItemAsync(SID_KEY);
  if (userProfile) {
    // DESTRUCTURE NEEDED DATA
    const { foto_de_perfil: profileImage, tags } = userProfile;
    // PUT REQUEST
    await onUpdate(
      { authToken: token as string },
      {
        profileImage,
        tags,
      }
    );
  }
};

It seems like this is the core of the issue you are facing, that the profileImage destructured and being passed in to onUpdate() here is stale (old, previous data)?

I suspect that this could be an issue unrelated to Pullstate itself, perhaps just a more regular issue in code that crops up from time to time. But I'd need a little more context to confirm.

Je12emy commented 3 years ago

Hello, thank you for your response! The variable userProfile is used for reading the state which allows me to render some properties in my view.

I had to check if it was initialized before mutating it since it could be of type null, sorry if this problem is unrelated to Pullstate in any way!

And yes, the profileImage which is being destructured is indeed stale, but it seems like after the onUpdate() function is done it shows the correct profile image but the put request has already been done with the stale data.

lostpebble commented 3 years ago

Ahh okay. I think I know what is happening here, and its quite a common issue that people run into with React and hooks- it all boils down to scope.

What I think is happening, is that even though you think you are getting a "fresh" value from:

// TEST FOR GLOBAL STATE
  const userProfile = UIProfile.useState();

This userProfile value was "fresh" only at the time you originally called this part of the code during the original component render.

When you call the updateProfile() function directly after updating the value inside your store:

const updateProfileImage = async (pickerResult: string) => {
  // UPDATE DOES NOT OCCUR ON STORE WITH THE PICKER RESULT URI
  UIProfile.update((p) => {
    if (p) {
      p.foto_de_perfil = pickerResult as string;
    }
  });

  await updateProfile();
};

The function updateProfile() as it exists right now, is still scoped to the earlier (now stale) value of userProfile. Its all about references, and what you are referring to inside the current scope and what value it currently holds.

userProfile is only changed on a future render of the component, and will be inside a new scope of the component then, and the new async functions you defined inside the component that time- hence when you call it again, it will run with that next value again (but still not really fresh, as expected after running your UIProfile.update()).

What you can do in this case is make the updateProfile() function refer directly to the current value inside the store by using getRawState():

const updateProfile = async () => {
  // GET THE AUTH TOKEN
  const token = await SecureStore.getItemAsync(SID_KEY);
  if (userProfile) {
    // DESTRUCTURE NEEDED DATA
    const { foto_de_perfil: profileImage, tags } = UIProfile.getRawState();
    // PUT REQUEST
    await onUpdate(
      { authToken: token as string },
      {
        profileImage,
        tags,
      }
    );
  }
};

This is generally not the best way to do it but yea, its not the end of the world and will work. Another option is to pass in the new value of the profile image as an argument to the function instead.

Hope that helps! Let me know if you still run into any issues.

Je12emy commented 3 years ago

Hello, I also managed to work around this with useEffect and a simple shouldUpdate flag. I'll make sure to give your solution a try.