When we want to fetch data, we are dealing with a promise, so we have three possible scenarios:
the promise resolves and it's a success
the promise resolves and it's a failure (dealt by
ErrorBoundary
component)the promise is in a pending state (dealt by
Suspense component
)
So this is how our code will look at a high level:
function App() {
return (
<>
<ErrorBoundary>
<Suspense fallback={<h1>Loading blog...</h1>}>
<BlogPost resource={resource1} />
<Share />
</Suspense>
</ErrorBoundary>
</>
);
}
BlogPost
it's a component which reads on the state of a promise:
function BlogPost({resource:{read}}) {
const {title,body} = read();
// ...
where resource
is an object created by function createResource
:
const resource1=createResource(fetchPosts(0))
Let's look at the definition of createResource
:
function createResource(promise){
let error,response
promise.then(r=>response=r).catch(e=>(error=e))
return {
read(){
if(error)throw error
if(response) return response
throw promise
}
}
}
As you see the read
function can throw a promise, an error or return a successful response. It's like if the promise would be catch by the Suspense
component and renders the fallback prop. The error would be catch by the ErrorBoundary
component, which we are going to see in a sec, and will render an ErrorScreen
component. When the promised is resolved successfully, the children of Suspense
will be rendered.
This is the ErrorBoundary
component:
import {Component} from 'react'
function ErrorScreen({error:{message}}){
return <div>
Something went wrong: {message}
</div>
}
class ErrorBoundary extends Component{
state={error:null}
static getDerivedStateFromError(e){
return {error:e}
}
render(){
const {children,fallback}=this.props
const {error}=this.state
if(error&&!fallback)return <ErrorScreen error={error} />
if(error)return <fallback error={error} />
return children
}
}
export default ErrorBoundary
As you see it works similarly to the Suspense
one, accepting a fallback
property, which in this case defaults to the render of ErrorScreen
component.
Let's see at the definition of fetchPosts()
function:
let blogPosts = [
{
id: 1,
title: 'qui est esse',
body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit'
},
];
export function fetchPosts(id) {
let post = blogPosts[id];
return new Promise((resolve) => {
setTimeout(() => {
console.log("fetched blogs");
resolve(post);
}, 2000);
});
}
As we can see, it returns a promise.
For completion, let's see at the definition of BlogPost
and Share
components:
function Share() {
useEffect(() => {
console.log("Effect Share");
return () => {
console.log("Cleanup Share");
};
});
console.log("Render Share");
return (
<div>Share:
<span> twitter</span>
<span> reddit</span>
</div>
)
}
function BlogPost({resource:{read}}) {
const {title,body} = read();
useEffect(() => {
console.log("Effect BlogPost");
return () => {
console.log("Cleanup BlogPost");
};
});
return (
<div>
<h1>Blog Post</h1>
<h3>{title}</h3>
<span>{body}</span>
</div>
);
}
And this is the output on the console when executing the app:
As you can see, with react 18 the Share
component doesn't render until the BlogPost
is ready, because they all belong to the children of the Suspense
component. If we used react 17 instead, this would be the output in the console:
As you can see, it renders first the Share
component before resolving the promise, and then, after the promise has been resolved successfully, it renders the BlogPost
component.
Top comments (0)