Open aralroca opened 5 years ago
Maybe a similar alternative as dangerouslySetInnerHTML
but for attachIsolatedEvents
...?
<img
{...props}
attachIsolatedEvents={{
onError: e => { e.target.src = fallbackSrc },
}}
/>
Is it helpful to use lazy load images?
Check the image then set it to src.
I don't have any problem with the lazy images. I'm using lazy images almost everywhere. However, in some main parts I want to load these images early as possible (in the SSR), to be rendered at the first moment.
Uh... I don't know much about SRR, can the server side check the correctness of the image URL?
@1010543618 I thought this as an option. But I dunno if It is possible.
I believe you are right about what happens.
Nevertheless, this code loaded from SSR, is working "sometimes"... I guess that this "sometimes" is because if the event is fired before the client side hydration. Is not catched by my JS
From this issue https://github.com/facebook/react/issues/12641
If that content finishes loading before ReactDOM hydrates, then the native onload event fires before React has connected any listeners to the element
There seems to be a racing condition between when hydration happens and when the browser throws the error event. If the browser already has thrown the error, I am not sure how a hydration config can help. Seems complicated, but I could be wrong.
Or maybe someone can help me providing any tip in order to fix this?
You could use ReactDOM.render
instead of ReactDOM.hydrate
. (I am not SSR expert, I might be wrong).
Or you could avoid onError
event and make your own validation
Here is a quick rough prototype, (might need adjustments, I am not a hooks expert either )
function isImageValid(src) {
let promise = new Promise(resolve => {
let img = document.createElement("img");
img.onerror = () => resolve(false);
img.onload = () => resolve(true);
img.src = src;
});
return promise;
}
function Img({ src, fallbackSrc, ...rest }) {
const imgEl = React.useRef(null);
React.useEffect(
() => {
isImageValid(src).then(isValid => {
if (!isValid) {
imgEl.current.src = fallbackSrc;
}
});
},
[src]
);
return <img {...rest} ref={imgEl} src={src} />;
}
// Render usage
<Img
alt='some image'
src="https://dummyimage.com/600x400/000/fff"
fallbackSrc="https://dummyimage.com/600x400/000/f00"
/>
Try removing the src property and the fallback should be applied.
@kunukn thanks for your answer.
Currently I did a workaround to fix these two scenarios:
However, the second scenario for me is not ideal because is adding the fallback image after hydration, and not before. So if the image is broken, first is loading the broken status of the browser, and then the fallback image.
This is my code:
Custom hook:
export default function useFallbackImageInSSR(fallbackSrc) {
const ref = useRef(null)
/**
* Error happened / catched after hydration
*/
const onError = useCallback(
e => { e.target.src = fallbackSrc }, [fallbackSrc],
)
/**
* Error happened before hydration, but catched after hydration
*/
useEffect(() => {
if (ref && ref.current) {
const { complete, naturalHeight } = ref.current
const errorLoadingImgBeforeHydration = complete && naturalHeight === 0
if (errorLoadingImgBeforeHydration) {
ref.current.src = fallbackSrc
}
}
}, [fallbackSrc])
return {
ref,
onError,
}
}
Usage:
const fallbackImageProps = useFallbackImageInSSR('/images/no-image.jpg')
return (
<img
alt="Example"
src="/images/non-existent-image.jpg"
{...fallbackImageProps}
/>
)
But again. This works, but is not 100% ideal.
So I guess, that the only way to do it ideal (adding the fallback image before the hydration), is attaching the event before hydration (as exception)... Maybe I'm wrong, but is the only way that I see to fix this 😕
Here's is one more approach/strategy.
When I see this code
function Img(props) {
return (
<img {...props} onError={e => { e.target.src = fallbackSrc } } />
)
}
It looks like we want to render the fallback image as fast as possible. As the browser parses the server-side rendered html, downloads the img, founds out it can't be rendered, applies the onerror
event and sets the src to a fallback image. I believe this is faster than parsing the JS, then apply the same logic.
. Here is a html version example with inline script.
<img src="imagenotfound.gif" alt="Image not found"
onerror="this.onerror=null;this.src='imagefound.gif';" />
This is from https://stackoverflow.com/a/9891041/815507
But you can't use the native onerror
on the img element in React. The closest thing I could get to similar result, where the fallback is executed from server-side html markup is this. I used an extra div markup.
function ImageSSR({ src, fallbackSrc, alt }) {
return (
<div
className="image"
dangerouslySetInnerHTML={{
__html: `
<img alt=${alt} src="${src}"
data-fallback=${fallbackSrc}
onerror="this.onerror=null;this.src=this.dataset.fallback;"
/>
`
}}
/>
);
}
// Render
<ImageSSR
src="https://dummyimage.com/200x100/000/fff&text=SSR"
fallbackSrc="https://dummyimage.com/200x100/000/f00&text=fallback"
alt="dummy image"
/>
If the src is invalid, then the fallbackSrc should kick in. I tested this with Chrome, Safari and Firefox and it worked for those.
Here's the codesandbox demo https://codesandbox.io/s/54xr4k8w4
I tried this with NextJS and confirmed the server-side rendered result.
<div class="image">
<img alt=dummy image src="https://dummyimage.com/200x100/000/fff&text=SSR"
data-fallback=https://dummyimage.com/200x100/000/f00&text=fallback
onerror="this.onerror=null;this.src=this.dataset.fallback;"
/>
</div>
@kunukn Thanks again for your answer.
Definitely I'm going to change my current workaround for your proposal. At least the fallback image is going to work as expected 🙂
Nevertheless, I miss a cleaner way to manage this. With your solution, it's hard to manage new props of ImageSSR Component, adding some extra complexity for each one. It also adds an extra div
node on top.
As a workaround to fix my problem, it works for me. However, I want to propose an improvement. I think a nice solution would be something like:
<img
{...props}
data-fallback={fallbackSrc}
attachHTMLProps={{
onerror: 'this.onerror=null;this.src=this.dataset.fallback;'
}}
/>
My particular problem is related to the onerror
event of image, but with the feature attachHTMLProps
it's also going to be possible to fix similar problems without adding a node wrapper and with less complexity.
What do you think about this?
that would be relly nice solution
Hi @kunukn and @aralroca, we have encountered the same initial issue at my job. The discussion here is quite helpful, but I have a few clarifying questions:
The demo found here: https://codesandbox.io/s/54xr4k8w4
Question: Is the demo intended to exemplify a solution where two components (ImageSSR and ImageClientSide) would need to be used? I feel I'm lacking some context regarding the demo's context - either just demoing what a SSR vs Client Side component may look like, or, implying both are part of a final solution.
Our solution: (and question) For clarity, this solution uses one component rendered server-side and client-side.
return (
<div dangerouslySetInnerHTML={{__html: `
<img
alt="${alt}"
src="${src}"
srcset="${srcSet}"
onerror="this.onerror=null;this.src='${fallback}';this.srcset='${fallback}';"
/>
`}} />
);
An aside: the notable difference is the addition of srcset
, and resetting srcset
in the onerror. This is necessary because of the behavior when both src
and srcset
are present. In our case, srcset
defines a 1x so that takes precedence.
Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-src
The server renders to a markup like this:
<img
alt="Description of image"
src="https://example.com/expected-image.png"
srcset="[long srcset here]"
onerror="this.onerror=null;this.src='https://example.com/fallback-image.png';this.srcset='https://example.com/fallback-image.png';"
/>
Great! Now the fallback is executed from server-side html markup. But there's one more thing...
When the fallback occurs, this code results in a console warning:
Warning: Prop
dangerouslySetInnerHTML
did not match. Server: [server markup] Client: [client markup]
This tripped me up, but after analysis I believe it is expected due to the scenario and can be safely ignored. The reason this warning occurs (I believe) is because the server-rendered markup's onerror
is invoked before hydration (client calling hydrate
), so once hydrate
is invoked, the now-mutated markup (due to fallback) doesn't match the original server-rendered markup that the hydrate thinks it should expect. Thus, the warning indicates the server is different than the client, and not the other way around.
Again, our conclusion is to ignore the warning, but still...
Question(s):
Does the reasoning and conclusion make sense, or is something amiss?
Sounds correct to me.
This is the warning I saw when fiddling with NextJS. Something like this.
const fallback = 'https://source.unsplash.com/random';
const src = '';
const srcSet = '';
index.js:1 Warning: Prop
dangerouslySetInnerHTML
did not match.Server: <img alt=\"random\" src=\"https://source.unsplash.com/random\" srcset=\"https://source.unsplash.com/random\" onerror=\"this.onerror=null;this.src="https://source.unsplash.com/random";this.srcset="https://source.unsplash.com/random";\">
Client: <img alt=\"random\" src=\"\" srcset=\"\" onerror=\"this.onerror=null;this.src="https://source.unsplash.com/random";this.srcset="https://source.unsplash.com/random";\">
The markup generation (HTML view source) was this:
<img
alt='random'
src=''
srcset=''
onerror='this.onerror=null;this.src="https://source.unsplash.com/random";this.srcset="https://source.unsplash.com/random";'
/>
To get around the warning, I tried to add setTimeout 100ms
and the warning went away. This is probably happening because the hydration steps in before the DOM is mutated.
// Not to be used.
<div dangerouslySetInnerHTML={{
__html: `
<img
alt='random'
src='${src}'
srcset='${srcSet}'
onerror='this.onerror=null;setTimeout((function(){this.src="${fallback}";this.srcset="${fallback}"}).bind(this),100);'
/>
`}} />
I would ignore the warning.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.
Hi, I'm testing a solution on NextJs SSR and seems to be working well, but it is to be very different from the ones above so I was curious to know your opinions about.
const Image = ({ id, src, alt, onLoad, onError, className, withLazy }) => {
const imageRef = useRef();
const [isVisible] = useIsVisible(imageRef);
const [imageSrc, setImageSrc] = useState(null);
useEffect(() => {
if (!withLazy) {
setImageSrc(src);
} else {
if (isVisible) {
setImageSrc(src);
}
}
}, [isVisible]);
const handleError = e => onError && onError(e);
const handleLoad = () => onLoad && onLoad();
return (
<img
id={id}
alt={alt}
src={imageSrc}
className={className}
ref={imageRef}
onLoad={() => handleLoad()}
onError={e => handleError(e)}
/>
);
};
Thanks
Hi, I'm testing a solution on NextJs SSR and seems to be working well, but it is to be very different from the ones above so I was curious to know your opinions about.
const Image = ({ id, src, alt, onLoad, onError, className, withLazy }) => { const imageRef = useRef(); const [isVisible] = useIsVisible(imageRef); const [imageSrc, setImageSrc] = useState(null); useEffect(() => { if (!withLazy) { setImageSrc(src); } else { if (isVisible) { setImageSrc(src); } } }, [isVisible]); const handleError = e => onError && onError(e); const handleLoad = () => onLoad && onLoad(); return ( <img id={id} alt={alt} src={imageSrc} className={className} ref={imageRef} onLoad={() => handleLoad()} onError={e => handleError(e)} /> ); };
Thanks
@andreapiras The problem that I see is that the image always is loaded after the hydration (after the useEffect). Can be a workaround, but it's interesting to find a solution to load the image earlier.
@andreapiras I agree with the previous comment. To state it differently, your code appears to avoid the issue discussed here because the img src is not set until the client has kicked in anyways. It's not server-side rendering to the full extent you probably want.
Is there somewhere you're calling renderToString
or renderToStaticMarkup
? If you log the result of that, I'm guessing your Image renders out to something like <img alt="i am alt" />
because as previously mentioned, src
won't be set until after hydration, on the client.
@andreapiras I agree with the previous comment. To state it differently, your code appears to avoid the issue discussed here because the img src is not set until the client has kicked in anyways. It's not server-side rendering to the full extent you probably want.
Is there somewhere you're calling
renderToString
orrenderToStaticMarkup
? If you log the result of that, I'm guessing your Image renders out to something like<img alt="i am alt" />
because as previously mentioned,src
won't be set until after hydration, on the client.
Yes, you are right, I've checked the generated source and the src was empty so the solution is not valid. Thanks for the feedback guys
this issue is particularly annoying when we need to fire onload metric for SSR image. The only work around for us is to write JS outside React to attach the event listener faster enough. We should provide support rendering for onload
attribute in SSR.
The only work around for us is to write JS outside React to attach the event listener faster enough.
FWIW that sounds like a reasonable solution to me. Specifically, a capture phase document-level listener for the load
event should work?
document.addEventListener('load', (e) => {
// ...
}, { capture: true })
as long as this runs before all other JS you should be able to record which ones have fired.
FWIW that sounds like a reasonable solution to me. Specifically, a capture phase document-level listener for the
load
event should work?document.addEventListener('load', (e) => { // ... }, { capture: true })
as long as this runs before all other JS you should be able to record which ones have fired.
Are you suggesting some kind of event delegation in that single global event listener? In our case, we are not looking at a document level load event, but the first load event fired by each image element above the fold. I don't think it is very easy to do it outside element level.
Besides, our img could be SSR or CSR, and I dont want to create a separate imgSSR. Here is my current idea:
In component level, I still attach the react event listener, but I will check a flag set on the native event e.defaultPrevented (load event cannot be cancelled:disappointed_relieved:)
function Image() {
const onLoad = (e) => {
if (!e.nativeEvent.hasFired) fire();
}
return <img onload={onLoad} />
}
And only for SSR rendered element, attach an inline script before hydration
<img /> // result of SSR
<script>
root.querySelector('img').addEventListener('load', function(e) {fire(); e.hasFired = true; })
</script>
BTW, for SyntheticEvent, can you tell me the difference among
e.isDefaultPrevented() vs e.defaultPrevented vs e.nativeEvent.defaultPrevented
I had similar issue, but solved my problem a bit differently.
Waring: it won't work for you if you need the onLoad/onError event, but if you just wan't to validate that the image is loaded it's just fine:
const PerformanceItem = () => {
const imageRef = useRef(null)
const isSSR = useIsSSR()
const [isPageReady, setPageReady] = useState(false)
const maybeInformPageReady = () => () => {
if (!isSSR && !isPageReady) {
triggerSomeAnalytics()
setPageReady(true)
}
}
const onMainImageLoad = maybeInformPageReady()
useEffect(() => {
const img = imageRef.current
if (!img || isPageReady) return
// This is needed on first page load after SSR, because onLoad doesn't get fired,
// but the img is supposed to be loaded on SSR, so we just validate it.
if (img.complete) {
onMainImageLoad()
}
// NOTE img.complete will be true for failed images as well,
// so if you want to check for img failure you can do:
if (img.complete && img.naturalWidth === 0) {
img.src = 'some fallback url'
}
}, [imageRef, isPageReady, onMainImageLoad])
return (
<img
src={someUrl}
onLoad={onMainImageLoad} // won't fire after page refresh due to SSR
onError={onMainImageLoad} // won't fire after page refresh due to SSR
onAbort={onMainImageLoad} // won't fire after page refresh due to SSR
ref={imageRef}
/>
)
}
I solved this issue on a Next.js project using these two functions during the server side rendering stage. Live codesandbox example.
const checkImageUrl = async (imageUrl: string): Promise<boolean> => {
try {
const response = await axios.get(imageUrl)
if (response.status === 200) return true
else return false
}
catch (error) {
return false
}
}
export const checkAndFormatImageUrlWithFallback = async (imageUrl: string, fallbackUrl: string): Promise<string> => {
const isValidImageUrl = await checkImageUrl(imageUrl)
return isValidImageUrl ? imageUrl : fallbackUrl
}
Hi! In case this could be useful to anyone, I fix this issues with the following code:
export const Img = ({
src = '',
onImageLoaded,
title,
alt,
...rest
}: Props) => {
const imageRef = useRef<HTMLImageElement | null>(null)
function handleOnLoad() {
if (imageRef.current?.currentSrc) {
onImageLoaded && onImageLoaded()
}
}
useLayoutEffect(() => {
if (imageRef.current?.currentSrc) {
preloadImageWithCallback({
imageUrl: imageRef.current.currentSrc,
callback: onImageLoaded,
})
}
}, [onImageLoaded])
return (
<img
src={src}
onLoad={handleOnLoad}
ref={imageRef}
title={title}
alt={alt}
decoding="async"
css={css`
width: 100%;
height: 100%;
object-fit: cover;
`}
{...rest}
/>
)
}
and
export async function preloadImageWithCallback({
imageUrl,
callback,
}: {
imageUrl: string
callback?(): void
}) {
function handleCallback() {
callback && callback()
}
const _image = new Image()
_image.src = imageUrl
if (_image.decode) {
await _image.decode()
handleCallback()
} else {
_image.onload = handleCallback
}
}
With the use of useLayoutEffect
, I handle the first render case by manually preloading the image, since the onLoad
will not be triggered.
The onLoad
method is only used in those cases when the image is lazy loaded.
You can see a real example here -> https://wildtrek.it The images are all lazy loaded, and when the image is fully loaded and ready to be appended to the DOM, I show them.
The only thing is that I keep getting the following warning:
useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format.
Does anyone know how to silence it?
Thanks, and hope this could be useful to anyone!
EDIT: About yow to silence it, I'm following this guide -> https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a
So, here's a workaround that works (for me)!
const Img: FC<
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>
> = ({ src, ...props }): JSX.Element => {
const [hasRendered, setHasRendered] = useState(false);
const imgRef = useRef<HTMLImageElement | null>(null);
useEffect(() => {
if (imgRef.current && hasRendered) {
imgRef.current!.src = src || '';
}
}, [src, hasRendered]);
useEffect(() => {
setHasRendered(true);
}, []);
return (
<img
{...props}
ref={imgRef as any}
alt={props.alt || 'image'}
aria-hidden={true}
onError={...}
onLoad={...}
/>
);
};
So, the magic happens in the two useEffect
hooks. (Using just one didn't work).
Basically, the second useEffect
ensures the first hook is triggered (or component re-renders) a second time (after initial render), due to the hasRendered
dep, which then forces the image src to be set in that hook which then triggers the events on the client!
There was back and forth what events were being replayed when hydrating in React 18. @gaearon is there still work planned or should we close with a summary?
@eps1lon @gaearon any updates on this?
There was back and forth what events were being replayed when hydrating in React 18. @gaearon is there still work planned or should we close with a summary?
@gaearon & @eps1lon. Do you know if there has been any progress on how this should be handled? I'm seeing what i believe is this issue when using React 18 components in a NextJS project that uses SSR.
Any updates on this im currently using an image to tell whether the client has loaded an image somewhere down the page if so it will load more data in.
Any updates on this issue ?
Any updates on this issue?
Recently I detected a problem using React in SSR. Maybe is quite obvious, but all the JavaScript events that are fired before the JavaScript is loaded, are lost...
This is something normal. The JS is not there yet... Ok. However, I expect to have some utility to do some exceptions.
I'm going to try to explain my case:
I'm using an image, and in the event
onError
, I want to change the src to render a fallback image.Nevertheless, this code loaded from SSR, is working "sometimes"... I guess that this "sometimes" is because if the event is fired before the client side hydration. Is not catched by my JS. And the
e => e.target.src = fallbackSrc
is never executed. However, if the JS is loaded faster than the onError event, is catched, and is rendering the fallback image as I expected.I want to propose some utility to do sometimes some exceptions, and render the JS inline on the first render. Perhaps, adding some extra config in
ReactDOM.hydrate
? I dunno...Or maybe someone can help me providing any tip in order to fix this?