May 28, 2021
Where to destructure props, and assign default values to them.
This is an interesting topic affecting developer experience: code duplication, code readability and cognitive load.
There are ways to define component props and associate default values to them.
A common approach is to destructure props in the function signature. Another approach is to destructure them in the function body.
// Destructuring in function signature
function Video({prop1, prop2}: TVideo) {...}
// Destructuring in function body
function Video(props: TVideo) {
const {prop1, prop2} = props
}
Associating default props comes with at least three different approaches.
// Associating default props in function signature
function Video({prop1, prop2}: TVideo = video) {...}
// Associating default props in function signature at destructuring
function Video({prop1 = 'prop1', prop2 = 'prop2'}: TVideo) {...}
// Associating default props in function body
function Video(props: TVideo) {
const {prop1, prop2} = merge(props, video)
}
Which approach is better? Which approach is complete? What makes an approach better than another?
To answer the questions let’s focus first on where to destructure. Then on how to associate default props. Then combine the findings into a verdict.
Destructuring leads to code duplication. In the case of a large number of props might lead to code readability problems. And depending on the scenario may take a toll on cognitive load.
Assuming type definitions are in place, destructuring leads inevitably to code duplication:
interface TVideo {
prop1: string,
prop2: string
}
// Duplication in function signature
function Video({prop1, prop2}: TVideo)
// Duplication in function body
function Video(props: TVideo) {
// The same amount of duplication as above
const {prop1, prop2} = props
// When not all props are needed ...
// ... this approach contains less duplication
const {prop1} = props
}
The advantage goes to the function body
approach. It can lead to less duplication.
Editors try to offer information about functions on hovering or clicking their name.
This comes handy when trying to use a function. It gives hints on usage and return value.
Editors vary in capability to display information on hover. In my experience VSCode performs better than Atom. Or Atom just needs a better plugin. Other editors might perform better than VSCode.
Differences in editor capabilities reduce the importance of this criteria.
For curiosity, and pursuing a better development experience, after playing with various scenarios in VSCode I found:
ctrl+hover
returns the first 10 lines of the function, as is.Based on the above there is no difference between the two destructuring approaches.
A hover when destructuring is in the function signature gives the same information as a ctrl+hover
when destructuring is in the function body.
In both cases the hint information is incomplete. There is no type information on prop1
, prop2
.
Before assigning default props to function props they should be set up to catch missing function props.
On assigning them, the prop structure defines which method to use. In case of a small number of flat props a pretty good usability can be reached.
Default props prevent errors when destructuring undefined props and trying to use them.
Destructuring is inevitable in nested props, and nested props will be present in your code sooner or later.
One can circumvent destructuring by using optional chaining.
// Optional chaining
// - On deeply nested props leads to trainwreck:
// prop1?.prop1a?.prop1aX?.prop1aX...
const text = prop1?.prop1a
const numbers = prop2?.prop2a?.map(item => item).join(',')
In the long term, when nesting goes deeper, destructuring scales better.
// Destructuring
// - On deeply nested props works fine
const { prop1, prop2 } = props
const { prop1a } = prop1
const { prop2a } = prop2
const numbers = prop2a?.map(item => item).join(',')
Optional chaining handles missing props. Destructuring fails with Unhandled Runtime Error
.
When prop1
is null
:
// Returns value `undefined`
const text = prop1?.prop1a
const { prop1, prop2 } = props
// Breaks with an error
const { prop1a } = prop1
To make destructuring error proof, default values must cover the full depth.
export interface TNested {
prop1?: {
prop1a: string,
};
}
export const nested: TNested = {
// This doesn't go into full depth.
// It will give an error on destructuring.
prop1: null,
}
export const nestedFullDepth: TNested = {
// This is ok.
prop1: {
prop1a: null,
},
}
Once error-proof and nested default props are set up — they should be assigned to function props.
In JavaScript object assignment works only with flat objects.
Nested objects need a special function to perform the same task.
Lodash offers such a function: defaultsDeep
, to recursively assign arbitrarily nested default props to function props.
Associating default props to props in function signature is not straightforward, might be impossible, or might require expert knowledge I don’t have at the moment.
// This gives the error:
// Parameter '{ prop1, prop2 }' cannot reference identifier 'prop1' declared after it.ts(2373)
function Video({prop1, prop2}: TVideo = defaultsDeep({prop1, prop2}, nestedFullDepth)) {...}
// This gives the error:
// No value exists in scope for the shorthand property 'prop1'. Either declare one or provide an initializer.
function Video({...defaultsDeep({ prop1, prop2 }, nestedFullDepth)}: TNested) {...}
Associating default props in function body just works:
// This works.
function Video(props: TVideo) {
const { prop1, prop2 } = defaultsDeep({ ...props }, video)
}
The advantage goes to the function body
approach. It works as is.
It’s time to revisit how editors display hover hints on functions now with default prop assignments.
function Video({prop1, prop2}: TVideo = video) {...}
ctrl+hover
displays the same amount of information.
function Video({prop1 = 'prop1', prop2 = 'prop2'}: TVideo) {...}
ctrl+hover
displays better information:
function Video(props: TVideo) {
const { prop1, prop2 } = merge(props, video)
}
ctrl+hover
displays better information:
Where to destructure props, and assign default values to them depends on the shape of the props.
When props are flat and small in number, destructuring in function signature + assigning default values at destructuring, wins. ctrl+hover
over the function name displays good enough usage information.
Nested props and the requirement to use a deep merging function implies destructuring in the function body.
The developer experience with ctrl+hover
over the function name is less pleasant: the default values, helping to infer the prop type, are not shown.
// Props are flat and small in number.
// This approach is recommended.
//
// This approach gives the best developer experience:
// On `ctrl+hover` the type of the props can be inferred from their default value.
function Video({prop1 = 'prop1', prop2 = 'prop2'}: TVideo) {...}
// Props are flat but large in number.
// This approach is not recommended.
function Video({
prop1 = 'prop1',
prop2 = 'prop2',
prop3 = 'prop3',
...
...
prop10 = 'prop10'
}: TVideo) {...}
// Props are flat but large in number.
// This approach is recommended for better code readability.
//
// On `ctrl+hover` this approach doesn't give hints about the type of the props.
function Video(props: TVideo) {
const propsMerged = defaultsDeep({ ...props }, defaultProps)
const { prop1, prop2 } = propsMerged
}
// Props are nested.
// This approach is recommended (perhaps the only viable option).
function Video(props: TVideo) {
const propsMerged = defaultsDeep({ ...props }, defaultProps)
const { prop1, prop2 } = propsMerged
}
To React with best practices. Written by @metamn.