Requisite React
When was the last time you saw an error and had no idea what it meant (and therefore no idea what to do about it)? Today? Yeah, you're not alone... Let's talk about how to fix that.
Let's analyze this React tree structure:
<App> <ShipSearch /> <ShipDetails /></App>
Let's assume that App
renders the ShipSearch
and ShipDetails
components,
passing props as needed.
ShipSearch
is a server component which takes a search
and uses that to
perform a database query:
async function ShipSearch({ search }) { const shipResults = await searchShips({ search }) // ... render stuff}
The ShipDetails
component is also a server component which takes a shipId
and uses that to perform a database query:
async function ShipDetails({ shipId }) { const ship = await getShip({ shipId }) // ... render stuff}
Because these are sibling components, both queries will run at the same time.
searchShips query ----> searchShips resultgetShip query ----> getShip result
That's all fine. But let's say the App
component needs to resolve a logged in
user:
async function App() { const user = await getLoggedInUser() // ... render stuff}
Uh oh, now we have a waterfall:
getLoggedInUser query ----> getLoggedInUser result searchShips query ----> searchShips result getShip query ----> getShip result
The issue here is now App
is waiting for getLoggedInUser
to resolve before
it can render anything. This means both searchShips
and getShip
won't be
executed until after getLoggedInUser
resolves, even though those don't
directly depend on the user.
This is a problem generally for React Suspense as well and I dive deep into that in the React Suspense workshop exercise 6.
There are a few ways to think about this problem:
Maybe it's not a problem if getLoggedInUser
is fast 🤷♂️ But that's not always a
sure thing, and even if it's fast today doesn't mean someone won't add something
else that will be slow tomorrow. Still, it's always good to think about how bad
ignoring the problem would actually be since there could be bigger fish to fry.
Alternatively, we can lift searchShips
and getShip
into App
and pass the
results down to ShipSearch
and ShipDetails
. Then we could use Promise.all
to make sure they run concurrently. But passing props can get annoying pretty
fast. We can use a library like
@epic-web/cachified
to dedupe the
queries and then App
simply kicks off the query earlier which would at least
be better.
Unfortunately, another issue though is this can get unweildy pretty fast,
especially if there's logic around which queries should run based on other
props. You'd have to move all that logic until your entire app lives in App
😱.
The next solution I can come up with is to use or build a compiler which can find all queries and preload them automatically. This is what Relay is. There are quite a few things I don't like about this approach though:
So I guess what I'm saying is I don't like any of these solutions. But I'd like to reframe the problem with more context.
Let's say we live in a world where we don't have RSCs or Suspense. You're just building an app with components that fetch data and render stuff.
In that world you've got three options:
Sound familiar? It's exactly the same problem we've always had with components and data. There's always been this tension between colocating and passing down data (prop drilling).
One of the nice things about Remix is that it gives you a fourth option:
This allows Remix to load data as soon as the request comes in regardless of whether the component has rendered.
This is what I've been doing for years and it's been awesome.
This blog post isn't really about why I think RSCs are awesome. You can read React Server Components: The Future of UI for that. Just know that composition at route boundaries is not as good as composition at component boundaries and I want React-level composition badly.
What I want to do is show how RSCs fit into the same tension between colocating and prop drilling that we've always had with components and data. There's no new problems here.
But what I find interesting is how a server-side waterfall is probably better than a client-side waterfall primarily because you get to control the network. The connection between your server rendering server and your database is probably stronger, faster, more reliable, and closer. Or maybe it's not, but the point is you are in control there and can make improvements to that if it's important to you. You also have more fine-grained control over caching (requests from separate clients can share a cache for common data).
When you have a client-side waterfall, you're dealing with the user's device and their network connection which may be great or may be terrible but you definitely have no control over.
So shifting this problem from the client to the server sounds like a net gain for many scenarios.
The "waterfall" problem of RSCs is not a new problem. It's the same tension between colocating and prop drilling that we've always had with components and data. Maybe there's a new solution we can look forward to in the future. I welcome ideas (preferrably something that doesn't require a special compiler)! Until then, I'm actually pretty happy with just making things fast enough that the problem isn't a problem.
Delivered straight to your inbox.
When was the last time you saw an error and had no idea what it meant (and therefore no idea what to do about it)? Today? Yeah, you're not alone... Let's talk about how to fix that.
Epic React is your learning spotlight so you can ship harder, better, faster, stronger.
I still remember when I first heard about React. It was January 2014. I was listening to a podcast. Pete Hunt and Jordan Walke were on talking about this framework they created at Facebook that with no support for two way data-binding, no built-in HTTP library, no dependency injection, and in place of templates it had this weird XML-like syntax for the UI. And to top it all off, I was listening to it while driving to the first ever ng-conf.
Why can't React just magically know what to do without a key?