- 
                Notifications
    You must be signed in to change notification settings 
- Fork 14
[add] Image source headers handling #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[add] Image source headers handling #11
Conversation
cf3bc09    to
    4e6daca      
    Compare
  
    | let uri; | ||
| const abortCtrl = new AbortController(); | ||
| const request = new Request(nextSource.uri, { | ||
| headers: nextSource.headers, | ||
| signal: abortCtrl.signal | ||
| }); | ||
| request.headers.append('accept', 'image/*'); | ||
|  | ||
| if (onLoadStart) onLoadStart(); | ||
|  | ||
| fetch(request) | ||
| .then((response) => response.blob()) | ||
| .then((blob) => { | ||
| uri = URL.createObjectURL(blob); | ||
| setBlobUri(uri); | ||
| }) | ||
| .catch((error) => { | ||
| if (error.name !== 'AbortError' && onError) { | ||
| onError({ nativeEvent: error.message }); | ||
| } | ||
| }); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried to write some unit tests covering the new functionality, but because of this logic a few mocks need to be setup and make the PR bigger than it has to be
I think it might be better to move this logic to ImageLoader.loadUsingHeaders so in unit tests we can stub that method and ensure it gets called when we intent to
It's easy to manually verify the fetch logic works and downloads content
Once that's clear it's easy to just verify loadUsingHeaders is called correctly with expected input
I've made a similar comment on the main repo: https://github.com/necolas/react-native-web/pull/2442/files#r1072080652
What do you think, should we refactor / write some tests now or let the mainstream repo request those changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm since this logic is the real meat & potatoes of the ImageWithHeaders component, if we move it to ImageLoader.loadUsingHeaders I assume we could get rid of this ImageWithHeaders component, and basically check if there's headers passed in the props - if yes, we call ImageLoader.loadUsingHeaders instead of ImageLoader.load here?
react-native-web/packages/react-native-web/src/exports/Image/index.js
Lines 269 to 294 in bd1f7b8
| requestRef.current = ImageLoader.load( | |
| uri, | |
| function load(e) { | |
| updateState(LOADED); | |
| if (onLoad) { | |
| onLoad(e); | |
| } | |
| if (onLoadEnd) { | |
| onLoadEnd(); | |
| } | |
| }, | |
| function error() { | |
| updateState(ERRORED); | |
| if (onError) { | |
| onError({ | |
| nativeEvent: { | |
| error: `Failed to load resource ${uri} (404)` | |
| } | |
| }); | |
| } | |
| if (onLoadEnd) { | |
| onLoadEnd(); | |
| } | |
| } | |
| ); | |
| } | 
If that's what you're thinking, I honestly do think that logic is cleaner, even without needing to write tests for Expensify's case so I'd say the refactor sounds like a nice idea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the idea is purely to ease mocking but I'll give your suggestion a try
Last PR tried to solve everything inside the same component but that resulted in more logic
What a component like ImageWithHeaders gives us is a guarantee that source would always be an object with headers
BTW there's feedback on the mainstream PR: necolas#2442 (comment) that we should write tests and thumbs up for extracting ImageLoader.loadUsingHeaders
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried to remove the ImageWithHeaders component and use either loadUsingHeaders or load functions here: https://github.com/Expensify/react-native-web/compare/master...kidroca:react-native-web:kidroca/feat/image-loader-headers-alt-2?diff=unified
But it results in a similar amount of changes and modifies some of the original logic (and seems harder to review)
- because now the source loading useEffectneeds to account for objects
- loadand- loadWithHeadersneed to work in a similar way in order to be interchangeable (so they were modified to return a- request.cancelfunction)
We might merge load and loadUsingHeaders instead of testing which one to run, though this would be similar to the first PR were load handled loading images with and without headers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing that refactor! Personally I prefer the new changes because it keeps all the image loading logic in ImageLoader and there's minimal changes to Image/index.js - I don't see those new changes too difficult to review (though I am not sure where lastLoadedSource gets updated).
I'd say let's move forward with this last refactor, as I think it's pretty straightforward compared to passing / not passing specific props to the BaseImage component required in this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the refactor is more of a proof that there are more "gymnastics" necessary in order to make this work by changing the original logic inside Image component
Original logic is changed, the cleanup logic is different, ImageLoader.load is changed, people, including the mainstream maintainer would have to inspect how these used to work and whether the change is suitable
IMO the mainstream maintainer already saw the update and is fine with just moving the fetch call to ImageLoader.loadUsingHeaders. I'm still trying different things, but the most straightforward PR so far seems to be the current one
There's also a big rework planned for the Image and the original loading logic would change, it would probably be best to not write stuff that depend on it (in the alt branch loadUsingHeaders depends on load)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me update the current PR with loadUsingHeaders extracted to ImageLoader and then we can make one final decision  which set of changes would work best for us
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the mainstream maintainer already saw the update and is fine with just moving the fetch call to
ImageLoader.loadUsingHeaders. I'm still trying different things, but the most straightforward PR so far seems to be the current one
Yeah this is one additional reason I really like this approach, even though there may be some additional changes in the upstream repo needed that we don't need at the moment in this fork 👍
Let me update the current PR with
loadUsingHeadersextracted toImageLoaderand then we can make one final decision which set of changes would work best for us
That sounds perfect, thanks so much @kidroca 👍
| In need of some Review feedback here | 
| Sorry I didn't have a chance to get to this today, @marcaaron do you mind giving this a first pass if you have time today? I will definitely review tomorrow no matter what 👍 | 
| Chatted 1:1 with @Beamanator. I'd love to see a good faith effort on this one before chiming in. It sounds like there are some higher priority things on Alex's plate that kept him from getting to this today. Taking that information... I've determined that my review is not urgently needed here and Alex has agreed to give a first review (I think). | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a whole I'd say these changes were much easy to follow, thanks for this PR @kidroca 👍
As I responded to your comment, I do think the refactor (moving ImageWithHeaders's loading logic into ImageLoader) could make this even more understandable, as long as that eliminates the need for the ImageWithHeaders component. Let me know what you think about my other comments below 👍
| let uri; | ||
| const abortCtrl = new AbortController(); | ||
| const request = new Request(nextSource.uri, { | ||
| headers: nextSource.headers, | ||
| signal: abortCtrl.signal | ||
| }); | ||
| request.headers.append('accept', 'image/*'); | ||
|  | ||
| if (onLoadStart) onLoadStart(); | ||
|  | ||
| fetch(request) | ||
| .then((response) => response.blob()) | ||
| .then((blob) => { | ||
| uri = URL.createObjectURL(blob); | ||
| setBlobUri(uri); | ||
| }) | ||
| .catch((error) => { | ||
| if (error.name !== 'AbortError' && onError) { | ||
| onError({ nativeEvent: error.message }); | ||
| } | ||
| }); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm since this logic is the real meat & potatoes of the ImageWithHeaders component, if we move it to ImageLoader.loadUsingHeaders I assume we could get rid of this ImageWithHeaders component, and basically check if there's headers passed in the props - if yes, we call ImageLoader.loadUsingHeaders instead of ImageLoader.load here?
react-native-web/packages/react-native-web/src/exports/Image/index.js
Lines 269 to 294 in bd1f7b8
| requestRef.current = ImageLoader.load( | |
| uri, | |
| function load(e) { | |
| updateState(LOADED); | |
| if (onLoad) { | |
| onLoad(e); | |
| } | |
| if (onLoadEnd) { | |
| onLoadEnd(); | |
| } | |
| }, | |
| function error() { | |
| updateState(ERRORED); | |
| if (onError) { | |
| onError({ | |
| nativeEvent: { | |
| error: `Failed to load resource ${uri} (404)` | |
| } | |
| }); | |
| } | |
| if (onLoadEnd) { | |
| onLoadEnd(); | |
| } | |
| } | |
| ); | |
| } | 
If that's what you're thinking, I honestly do think that logic is cleaner, even without needing to write tests for Expensify's case so I'd say the refactor sounds like a nice idea
Move header loading logic here
51f7e9a    to
    739c02b      
    Compare
  
    739c02b    to
    93df02a      
    Compare
  
    There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved logic to ImageLoader.loadUsingHeaders
✅ PR ready for review
And here's an alternative version where we try to use a single Image component instead of creating BaseImage and ImageWithHeaders: https://github.com/Expensify/react-native-web/pull/13/files
cc @roryabraham
| const request = React.useRef<LoadRequest>({ | ||
| cancel: () => {}, | ||
| source: { uri: '', headers: {} }, | ||
| promise: Promise.resolve('') | ||
| }); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ImageLoader.loadUsingHeaders returns a request with reference to the last loaded source, and a cleanup function. We no longer need to capture lastLoadedSource and cleanup refs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kidroca thanks for much for keeping this PR around while also showing what #13 would look like - to be honest I am now thinking the ImageLoader changes look much simpler in this PR, and even I like how the logic for the ImageWithHeaders is separated out, so I'm actually go back on my previous thought, and I'd say this is good to merge 👍
| @marcaaron I'll leave this unmerged for today in case you want to check out this one vs #13 - if you don't have any concerns (or if you're too busy today) I'm pretty confident we can move forward here so I'll merge tomorrow | 
| Sounds good - I will take a look now! Also, just wanted to clear the air on this comment. Chatted 1:1 with Alex and it's clear we had different expectations about how this PR should be led and who should do the bulk of the reviewing. In retrospect, I should have clarified that stuff before leaving that comment. And it was wrong to imply that there were any bad intentions (definitely did not mean for it to come across that way). It also didn't need to be shared in public. Sorry Alex! | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks great. Love this new direction. I only have a few style notes and requests to improve the comments in the code.
|  | ||
| const propsToPass = { | ||
| ...props, | ||
| // Omit `onLoadStart` because we trigger it in the current scope | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not too sure what this comment means. Is there a different way to say this?
I think it's something like - the BaseImage onLoadStart event is not exposed to the parent. We are only interested in when the source with headers starts loading and not when the BaseImage loading starts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more like: ImageWithHeaders already calls onLoadStart when it starts loading the image, so we don't want the BaseImage to trigger that function a second time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I look at it I see it's confusing - it's like Alex said - loading starts inside ImageWithHeaders, to prevent BaseImage to raise onLoadStart a second time we filter it out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok cool I think we are all saying the same thing - BaseImage will not use an onLoadStart callback when we have headers.
| I'll apply the suggestions to the PR and post back in a minute | 
Co-authored-by: Marc Glasser <[email protected]>
7e2d106    to
    bb25c15      
    Compare
  
    | @Beamanator @marcaaron I've also updated the PR in App Expensify/App#13036 to use the latest changes made here | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two more tiny questions on comments
| Oh and one more question / possible point of confusion: 
 
 Just making sure my understanding is correct 😅 | 
Co-authored-by: Alex Beaman <[email protected]>
| 
 I think you've got it right When we load image with headers the  We only need to take care of the  | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the work on this @kidroca 👍 👍
| Published in v0.18.11 👍 | 
| @Beamanator @kidroca can you please include a link to the upstream PR in the description to make it easier to track this change in the upstream? | 
| @roryabraham sure thing - the upstream PR is: necolas#2442 (I added to the OP too!) | 
Upstream PR: necolas#2442
Details
Extend
ImageLoaderfunctionality to be able to work with image sources containing headersWe preserve the existing strategy that works with
image.srcfor cases wheresourceis just an
uriwith no headersWhen
sourcecontain headers we make afetchrequest and then render a local url for thedownloaded blob (
URL.createObjectURL)Fixed Issues
$ Expensify/App#12603
Test Strategy
Verify existing Image functionality has no regressions
npm run dev -w react-native-webandnpm run dev -w react-native-web-examplesImage: http://localhost:3000/imagemasterbranch. You can switch back and forth and verify the image are loading the same wayVerify Images with headers can be loaded
npm run dev -w react-native-webandnpm run dev -w react-native-web-examplesImage: http://localhost:3000/imagesourceWithHeadersherepackages/react-native-web-examples/pages/image/index.jsand try to load images from a server that expects a GET request with a header