New component utility functions #1578
Replies: 2 comments
-
This is an awesome write up! A few notes:
|
Beta Was this translation helpful? Give feedback.
-
I normally like inline comments to help narrate the point, but I didn't want to distract from the difference with comments. |
Beta Was this translation helpful? Give feedback.
-
createComponent
createComponent
was created as a lightweight wrapper aroundReact.forwardRef
that added handling for theas
prop. This utility function's largest value-add is how it handles polymorphic types. For example:This polymorphism is also why we stopped exporting the HTML attribute interface on component props. We introduced the
ExtractProps
instead:ExtractProps
knows what the default HTML interface of a component is and also allows you to override it if needed if your component uses anas
and is not polymorphic itself.Common Patterns
Compound components with models all follow a common pattern which meant common boilerplate. This is more noticeable with recent work to remove boilerplate from models. In compound components with a model, there are 2 types: a container component and a subcomponent. Container components always create a model's context and provide it to the React render tree. If a
model
is passed to a container, it will use it - if not, the container will create a model. A subcomponent, by contrast, will take a passedmodel
from props, or extract it from React's context.This led to boilerplate and also an issue with circular imports. A container component imports all subcomponents and exposes them using dot notation:
Webpack 4 handles this circular import, but not all bundlers do.
Container components
Let's look at the boilerplate common to all container components:
We'll notice a few oddities.
children
andmodel
in the prop interface even though all container components have these props. This allows room for error.useDefaultModel
so that theelemProps
object will only be props to be handed to the<Element>
componentelemProps
could contain aref
ifuseContainerComponent
needed to create a forked refMyModel
orMyModelConfig
types!Here's what it will have to look like with the new models:
The model no longer exports the types we need to create a container component, so we have to recreate them. This is a pain. We've added more information to the
useMyModel
hook -useMyModel.Context
,useMyModel.defaultConfig
, anduseMyModel.requiredConfig
, we can make a container factory function to replacecreateComponent
that takes in a model hook and adds all the extra types for us:Introducing
createContainer
Here are the benefits:
model
andchildren
are no longer needed in theContainerComponentProps
interface. The prop interface can focus on props specific to this componentref
handlingmodelHook
takes care of model value and types, we no longer need to create alocalModel
variableelemPropsHook
allows us to keep the render function focused on only rendering. We no longer need theelemProps
/props
variablesmodel
that provides the model if we need it, but this example does notThe external interface of
ContainerComponent
includes the optionalmodel
prop and all optional model config automatically without us having to type it!The funky part left in this example is the
<ContainerComponentProps>((elemProps, Element) => {})
. We do this becauseelemProps
contains additional props added by theelemPropsHook
, includingchildren
. This syntax is a little foreign at first, but it allows us to maximize Typescript's inference while minimizing how much Typescript code we need.Subcomponents
Subcomponents had similar, but different boilerplate to container components. A subcomponent would receive an optional
model
and use that model or grab from context. To avoidrules-of-hooks
ESLint errors, we used auseModelContext
hook that did this model handling for us.Using
createComponent
with current modelsIntroducing
createSubcomponent
createSubcomponent
takes care of the model finagling we had to do as well as the prop dance. TheSubComponentProps
can focus on any unique props the component takes that effects rendering. LikecreateContainer
, an optional 3rd parameter is themodel
in case your render function needs access to it. Most likely this is because your render function renders more than a single element, so theelemPropsHook
doesn't quite meet your needs.When to use
createComponent
createComponent
is not going away. It is still useful if you want to create a polymorphic component that forwards aref
andelemProps
. Use it if your component doesn't use models. For example, our button components don't have models and still usecreateComponent
. The new utility functions are useful in creating compound components with models that match our specifications without extra boilerplate.Beta Was this translation helpful? Give feedback.
All reactions