last updated: July 27, 2022
8 minute read
Data Fetching in React: Parent-Agnostic vs Parent-Dependent Children
Data fetching in React is a popular topic of blog posts, dev talks, and tutorials – and for good reason. Asynchronous data fetching introduces a whole collection of complexities to handle. I hope to contribute with a less-often discussed topic: the role of the children whose parent fetches data asynchronously, and to what extent the children should account for this behavior.
Preamble: basic inline null checking
Take the following snippet:
interface Data {price: number;}function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {const load = async () => {try {const data: Data = await getData();setData(data);} catch (e) {console.error(e);}};load();}, []);return null;}
Now consider a case where the Parent
wants to render markup based on the data fetched with
getData
.
function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {// ...}, []);return <>{data.price}</>;}
Because a useEffect
runs after the render, there's at least one render (i.e. the first) where
data
is still its initial value, null
. This means, one way or another, we have to account for a
null
instance of data
– otherwise, React will crash when we try to access (null).number
.
I'd highly recommend checking out Dan Abramov's A Complete Guide to
useEffect if the mechanics of useEffect
aren't so clear to you.
One popular option is to perform inline null checking.
function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {// ...}, []);return <>{data?.price}</>;}
With optional chaining in place, when data
is null
, accessing data?.price
will return
undefined
instead of throwing an error. This pattern takes advantage of the fact that, if an
expression passed to a {}
in React "resolves" to null
or undefined
, React will gracefully
(i.e. without throwing an error) render nothing.
Alternatively, we could return early and avoid our optional chaining altogether:
function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {// ...}, []);if (data === null) return;return <>{data.price.toFixed(2)}</>;}
Either solution works great when we're dealing with a single component, but once we introduce
children into the mix, we encounter some deeper questions. Consider our original example, but with a
new FormatNumber
child.
function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {// ...}, []);return <FormatNumber num={data?.price} />;}function FormatNumber({ num }: { num: number }) {return <>{num.toFixed(2)}</>;}
Notice that this code will crash on the first render when data?.price
resolves to undefined
, and
we pass undefined
as num
to FormatNumber
.
There are three possible fixes, two of which we already discussed:
function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {// ...}, []);// option 1 (old: return early)if (data === null) return;return <FormatNumber num={data.price} />;// option 2 (old: inline null checking, this time with a fallback value)return <FormatNumber num={data?.price ?? 0} />;}function FormatNumber({ num }: { num: number }) {// option 3 (new: null checking in the child)if (typeof num !== "number") return;return <>{num.toFixed(2)}</>;}
Now that we're dealing with two components, we have the option to null check in the parent or in the child – option 1 and option 2 null check in parent, and option 3 null checks in the child. This leads us to a deeper, more interesting question on best-practices:
Who should be responsible for null checking? The parent, or the child? This question is at the crux of this article. Let's explore both approaches through a differentiation I think helpful:
Parent-dependent children
A Parent-dependent child relies on its parent to do its null checking for them. It has no null checking of its own, and it expects its props to always be populated and of the correct type. Otherwise, it may throw.
In our example above, this would mean the Parent
should return early, ensuring that when
FormatNumber
is rendered, its num
prop is always populated and a number
. Alternatively, the
Parent
could perform inline null checking with a fallback value and achieve the same result.
It's worth noting that if the child's prop is a more complicated data structure, such as a series of
nested objects, passing a fallback value like ?? 0
isn't always an option.
Parent-agnostic children
In contrast to a Parent-dependent child, a Parent-agnostic child expects no null checking from its parent. Maybe the parent will null check before rendering the child, maybe it won't - the Parent-agnostic child won't rely on either behavior. Instead, the Parent-agnostic child will perform its own null checking, gracefully handling every case on its own.
In our example above, FormatNumber
would have its own null checking, maybe something like:
function FormatNumber({ num: number }) {if (typeof num !== "number") return;return <>{num.toFixed(2)}</>;}
Note: when null checking a number or string, a simple if (!prop)
check is most likely not
what you want – it will return false
with 0
or ""
!
So which is best?
I would argue that, in an ideal world, we would all write Parent-dependent children. The child
shouldn't have to worry about being passed nullish (null
or undefined
) values: it clearly
defines that it expects non-nullish props in its type! If you're consuming an api (i.e. a child
component) in your parent, it should be your responsibility to call that child with the props it
expects.
Unfortunately, this approach isn't always feasible. In the same way we're taught to never trust, and always validate, user input, the author of a child component should never expect that the parent will call it correctly. Anything else would be too optimistic.
But what if there was a way for the child to guarantee that the parent would pass it the correct
props, without needing any extra code to verify the fact? It seems too good to be true, but that's
exactly what we can do with
strictNullChecks
enabled.
Strict null checks
In the words of the TypeScript docs:
When strictNullChecks
is true
, null
and undefined
have their own distinct types and you’ll
get a type error if you try to use them where a concrete value is expected.
With strictNullChecks
enabled, let's go back to our example and try to perform inline null
checking in the parent:
function Parent() {const [data, setData] = useState<Data | null>(null);useEffect(() => {// ...}, []);/*Compile time ts error:Type 'number | undefined' is not assignable to type 'number'.Type 'undefined' is not assignable to type 'number'.*/return <FormatNumber num={data?.id} />;}// Parent-dependent variation (without it's own null checks)function FormatNumber({ num: number }) {return <>{num.toFixed(2)}</>;}
TypeScript now gives us a compile time error that data?.id
may be undefined
, and
FormatNumber
's num
prop can only be a number
!
To fix this error, we can either return early, or modify our inline null checking to use a fallback value.
It's important to note that, from the perspective of the parent, little has changed: you still have to return early or perform inline null checking. TypeScript won't save you any code, or advise you to choose one over the other. All of that's true.
But from the perspective of the child, this makes a huge difference. With strictNullChecks
enabled, the child can have the clean code of a Parent-dependent child (i.e. without any null checks
of its own) with the safety guarantees of a Parent-agnostic child – if a parent tries to pass the
child a prop not explicitly described in the child's type (like a nullish prop), TypeScript will
throw an error!
When strict null checks aren't available
strictNullChecks
are a great solution when your codebase in written in TypeScript, but for when
you're working in JavaScript, or for one reason or another you have to keep strictNullChecks
disabled, there are a few considerations I try to keep in mind when deciding between writing
Parent-dependent and Parent-agnostic children.
Is the child a general-purpose component designed to be used by multiple parents, or a one-off written for a single case? If the former, I tend to air on the side of writing Parent-agnostic children – as we discussed above, it's best not to rely on a consumer of your component null checking for you (whether that be yourself or someone else working on the codebase).
On the other hand, if the child is only used by a single parent, that may be a better case for writing a Parent-dependent child. The mental effort of remembering to pass non-nullish values to the child (either by returning early, or passing a fallback value when performing inline null checking), may be worth the code saved by making the child Parent-dependent.
It's not ideal, but for situations where we have to rely on best practices instead of compile-time checks, all you can do is use your best judgment.
you might also like:
How I Built My Blog
May 05, 2023
The stack I chose, or how I learned to stop worrying and love Next.js