-
Notifications
You must be signed in to change notification settings - Fork 15
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
[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
useEffect
needs to account for objects load
andloadWithHeaders
need to work in a similar way in order to be interchangeable (so they were modified to return arequest.cancel
function)
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
loadUsingHeaders
extracted toImageLoader
and 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 <marc.aaron.glasser@gmail.com>
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 <dabeamanator@gmail.com>
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
ImageLoader
functionality to be able to work with image sources containing headersWe preserve the existing strategy that works with
image.src
for cases wheresource
is just an
uri
with no headersWhen
source
contain headers we make afetch
request 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-web
andnpm run dev -w react-native-web-examples
Image
: http://localhost:3000/imagemaster
branch. 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-web
andnpm run dev -w react-native-web-examples
Image
: http://localhost:3000/imagesourceWithHeaders
herepackages/react-native-web-examples/pages/image/index.js
and try to load images from a server that expects a GET request with a header