Reduce Boilerplate with new component functions #1548
NicholasBoll
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Overview
createComponent
did a lot to reduce the Typescript required to create polymorphic components, but doesn't reduce boilerplate to create new components. Models have a lot of boilerplate Typescript. I'd like to introduce new components that reduce boilerplate and the amount of Typescript required. Find out more in our Compound Component DocsModels
Models are responsible for
state
andevents
and are the shared between the subcomponents. It is the explicit interface that subcomponents use to communicate. A model's events have a contract - when an event is called, it will first call the event's guard (should*
by convention) function, then the event's state-updating function, then the callback (on*
by convention) function. It works like this:That extra tracking for guards and callbacks was a lot of boilerplate. Some utility functions were created to make it easier but had their own boilerplate. Typescript 4.1 introduces Template Literal Types that allowed us to create new utility functions that removed the boilerplate required for guards and callbacks.
Before
canvas-kit/modules/preview-react/form-field/lib/hooks/useFormFieldModel.tsx
Lines 1 to 62 in 6670776
Before we had to create types for
*State
,*Events
,*Model
, create an event map,*BaseConfig
,Config
, and useuseEventMap
for the model'sevents
. And this doesn't solve the problem where a container component could have an element. In this case, the author would have to manually create an array of config keys to remove fromprops
to getelemProps
.After
canvas-kit/modules/preview-react/form-field/lib/hooks/useFormFieldModel.tsx
Lines 1 to 42 in 47a167f
The update is to drop the Typescript boilerplate and create a model's config with an object. There's
defaultConfig
andrequiredConfig
. ThedefaultConfig
serves 2 purposes - provide default values that are easy to determine, and extract type information from the config value.requiredConfig
is a little interesting though. Only the type is useful to the function and not the value.If you need to use a type that isn't inferred by Typescript, you'll need to use the
as
keyword to cast to a narrower type.If you want an optional config without a default, you'll have to cast the type to include
undefined
. This case isn't very common. Many times a default can be a falsey value instead and still get a type. For example,0
,false
, or an empty string result in a falsey comparison but still have the correct inferred type.Container components
Container components are responsible for setting up model context for subcomponents. They may or may not represent an element.
Before:
canvas-kit/modules/preview-react/form-field/lib/FormField.tsx
Lines 1 to 58 in 6670776
From this example shows a few main parts:
Props
interface,React.Context
, and the component definition. The props needs to contain amodel?:
prop and extend the model'sConfig
type. Then is must set up the model context and create aProvider
in the render body. This is common to all container components with a model.After:
Since all container components do the same thing, it is easier to create a custom utility function that does the boilerplate for us using a model hook as an input to both set up the
model
prop type and create a model context. Here's the after:canvas-kit/modules/preview-react/form-field/lib/FormField.tsx
Lines 1 to 43 in 47a167f
The main differences is removing the model context reference from the container component file, and removing the
Provider
from the render function. Notice we've also removedref
.ref
is provided on theelemProps
object always. We now ensureref
is always there rather than depending on theelemPropsHook
called. This gets rid of avoidable bugs.Subcomponents
Subcomponents get their model from React's context and almost always have an
elemPropsHook
to getelemProps
for the element in the render function. There was similar boilerplate to container components, but slightly different boilerplate.Before
canvas-kit/modules/preview-react/form-field/lib/FormFieldInput.tsx
Lines 1 to 21 in 6670776
We can see again we need to handle the
model
prop and it's type. There's something weird about the model's context - it comes from the container component, but the container component imports this subcomponent creating a circular dependency. This issue resolves with Webpack, but may cause issues with other bundlers. We also have to useuseModelContext
and renamemodel
tolocalModel
to avoid linting errors... We could do better.After
canvas-kit/modules/preview-react/form-field/lib/FormFieldInput.tsx
Lines 1 to 14 in 47a167f
The
createSubcomponent
function allows us to focus more on the unique configuration of our componentBeta Was this translation helpful? Give feedback.
All reactions