Using default properties in React
Introduction
When passing properties in React application one usually chooses between so-called prop drilling and React contexts. Both have their cons and pros. Prop drilling means passing the properties down from the parent to the child component. It has the advantage that the parent component can control the values that are used by the child component but can lead to very verbose code. The opposite is true when contexts are used: concise code but no control over the properties of the child component. In this blog post, I will describe a third approach based on Typescript that combines the first two: either the property value is received from the parent component or else the value comes from a DefaultPropsContext. I will refer to this as the “default properties” approach. The source code that implements this approach is available on github and can be installed via npm. In a follow-up post I will describe how you can use default properties in data containers.
Note: the “default properties” approach is designed to be compatible with MobX. The code is completely independent from MobX though, it only requires React.
The withDefaultProps function
In the “default properties” approach a component declares two types which - by convention - are called PropsT and DefaultPropsT. The component receives a list of properties props
that has been enriched by merging in the default properties:
import { withDefaultProps, stub } from "react-default-props-context";
type PropsT = {
name: string,
};
const DefaultProps = {
color: stub as string,
}
const MyComponent = withDefaultProps<PropsT, typeof DefaultProps>(
(props: PropsT & DefaultPropsT) => {
// The color value comes either from props.color or from a DefaultPropsContext.
return <text color={props.color}>Hello</text>;
}, DefaultProps
);
The withDefaultProps
function is a higher order function that adds the support for default properties. It creates a new properties object (that is passed into the wrapped component) that has a special lookup function. This lookup function will first try to resolve the property using the input argument of MyComponent. If unsuccessfull it will resolve the property by looking for a getter function in the nearest DefaultPropsContext. If still unsuccessfull then undefined
is returned.
At this point, we need to clarify a few things:
- how are the default property values provided?
- why are default properties based on getter functions and not just on values?
- how should a property be passed in when we don’t want to use the default value in the child component?
To answer these questions, take a look at another example:
import { observer } from "mobx-react";
import { DefaultPropsContext } from "react-default-props-context";
const MyFrame = observer(() => {
const foo = getFoo();
const defaultProps = {
color: () => "red",
bar: () => foo.bar,
};
return (
<DefaultPropsContext.Provider value={defaultProps}>
<MyComponent name="example" color="green"/>
</DefaultPropsContext.Provider>
)
})
The DefaultPropsContext provides the dictionary of default property values to the withDefaultProps
function. To override the default color
we can set this property
explicitly on MyComponent
. You will get the usual Typescript warnings if you are trying to set a property that does not exist (as a regular property or as a default property).
Now let’s talk about the getter functions in the the defaultProps
object. Since functions are more flexible than values this adds a
bit of additional complexity and power. The real reason though for having them has to do with MobX. If we were to copy the foo.bar
value directly into defaultProps
then MobX would notice that we are referencing it. That would mean that MyFrame
(which is the
component that creates the defaultProps
object) might be rendered more often than necessary. By using a getter function we avoid referencing foo.bar
.
Using nested contexts
One of the features of DefaultPropsContext
is that it allows nesting. Any nested DefaultPropsContext
instance should extend and
override the default properties set by any parent DefaultPropsContext
instances. You can code this manually by merging dictionaries
but a more convenient way is to use the DefaultPropsProvider
component. In the example below, we add an additional instance
of MyComponent
with name “exampleInner” that uses an updated and extended set of default properties (where the color is “blue” and
there is an additional baz
property).
import { observer } from "mobx-react";
import { DefaultPropsProvider } from "react-default-props-context";
const MyFrame = observer(() => {
const foo = getFoo();
const defaultProps = {
color: () => "red",
bar: () => foo.bar,
};
const defaultPropsInner = {
color: () => "blue",
baz: () => foo.baz,
};
return (
<DefaultPropsProvider value={defaultProps}>
<MyComponent name="example"/>
<DefaultPropsProvider value={defaultPropsInner}>
<MyComponent name="exampleInner/>
</DefaultPropsProvider>
</DefaultPropsProvider>
)
})
What happens when a default property is overwritten?
In one of the examples above we used color="green"
to override the default value of color
. This has the effect of (automatically) inserting a DefaultPropsProvider
that provides the new value. This means that also children of the receiving component see the overridden value!
Discussion
The benefit of using default properties as described is that it avoids prop drilling, while still allowing you to customize the property
values that a child component uses. By using nested DefaultPropsContext instances, you have a lot of flexibitily in deciding which parts
of the render tree see which default values.
This flexibility comes at a price though: default properties can originate from any DefaultPropsContext instance higher up in the tree.
In this sense, it is different from most other React contexts. To put it in another way: when a component declares that it accepts a
default property, it doesn’t care where this property comes from, as long as it’s provided by a DefaultPropsContext. If it tries to use a
value that no DefaultPropsContext provides, then it will receive undefined
(Typescript cannot help us with a warning there).
In my experience, the benefits easily outweight the drawbacks. DefaultPropsContext
allows you to get information to the place where it is
required, without the need to jump through hoops. This results in shorter and more readable code, that is easier to refactor. So if prop
drilling in your code gets out of hand, I would recommend to give this a try.