Tag: ​​Avoiding

Avoiding the Pitfalls of Nested Components in a Design System

When creating a component-based, front-end infrastructure, one of the biggest pain points I’ve personally encountered is making components that are both reusable and responsive when there are nested components within components.

Take the following “call to action” (<CTA />) component, for example:

On smaller devices we want it to look like this:

This is simple enough with basic media queries. If we’re using flexbox, a media query can change the flex direction and makes the button go the full width. But we run into a problem when we start nesting other components in there. For example, say we’re using a component for the button and it already has a prop that makes it full-width. We are actually duplicating the button’s styling when applying a media query to the parent component. The nested button is already capable of handling it!

This is a small example and it wouldn’t be that bad of a problem, but for other scenarios it could cause a lot of duplicated code to replicate the styling. What if in the future we wanted to change something about how full-width buttons are styled? We’d need to go through and change it in all these different places. We should be able to change it in the button component and have that update everywhere.

Wouldn’t it be nice if we could move away from media queries and have more control of the styling? We should be using a component’s existing props and be able to pass different values based on the screen width.

Well, I have a way to do that and will show you how I did it.

I am aware that container queries can solve a lot of these issues, but it’s still in early days and doesn’t solve the issue with passing a variety of props based on screen width.

Tracking the window width

First, we need to track the current width of the page and set a breakpoint. This can be done with any front-end framework, but I’m going using a Vue composable here as to demonstrate the idea:

// composables/useBreakpoints.js  import { readonly, ref } from "vue";  const bps = ref({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }) const currentBreakpoint = ref(bps.xl);  export default () => {   const updateBreakpoint = () => {        const windowWidth = window.innerWidth;          if(windowWidth >= 1200) {       currentBreakpoint.value = bps.xl     } else if(windowWidth >= 992) {       currentBreakpoint.value = bps.lg     } else if(windowWidth >= 768) {       currentBreakpoint.value = bps.md     } else if(windowWidth >= 576) {       currentBreakpoint.value = bps.sm     } else {       currentBreakpoint.value = bps.xs     }   }    return {     currentBreakpoint: readonly(currentBreakpoint),     bps: readonly(bps),     updateBreakpoint,   }; };

The reason we are using numbers for the currentBreakpoint object will become clear later.

Now we can listen for window resize events and update the current breakpoint using the composable in the main App.vue file:

// App.vue  <script> import useBreakpoints from "@/composables/useBreakpoints"; import { onMounted, onUnmounted } from 'vue'  export default {   name: 'App',      setup() {     const { updateBreakpoint } = useBreakpoints()      onMounted(() => {       updateBreakpoint();       window.addEventListener('resize', updateBreakpoint)     })      onUnmounted(() => {       window.removeEventListener('resize', updateBreakpoint)     })   } } </script>

We probably want this to be debounced, but I’m keeping things simple for brevity.

Styling components

We can update the <CTA /> component to accept a new prop for how it should be styled:

// CTA.vue props: {   displayMode: {     type: String,     default: "default"   } }

The naming here is totally arbitrary. You can use whatever names you’d like for each of the component modes.

We can then use this prop to change the mode based on the current breakpoint:

<CTA :display-mode="currentBreakpoint > bps.md ? 'default' : 'compact'" />

You can see now why we’re using a number to represent the current breakpoint — it’s so the correct mode can be applied to all breakpoints below or above a certain number.

We can then use this in the CTA component to style according to the mode passed through:

// components/CTA.vue  <template>   <div class="cta" :class="displayMode">          <div class="cta-content">       <h5>title</h5>       <p>description</p>     </div>          <Btn :block="displayMode === 'compact'">Continue</Btn>        </div> </template>  <script> import Btn from "@/components/ui/Btn"; export default {   name: "CTA",   components: { Btn },   props: {     displayMode: {       type: String,       default: "default"     },   } } </script>  <style scoped lang="scss"> .cta {   display: flex;   align-items: center;      .cta-content {     margin-right: 2rem;   }    &.compact {     flex-direction: column;     .cta-content {       margin-right: 0;       margin-bottom: 2rem;     }   } } </style>

Already, we have removed the need for media queries! You can see this in action on a demo page I created.

Admittedly, this may seem like a lengthy process for something so simple. But when applied to multiple components, this approach can massively improve the consistency and stability of the UI while reducing the total amount of code we need to write. This way of using JavaScript and CSS classes to control the responsive styling also has another benefit…

Extensible functionality for nested components

There have been scenarios where I’ve needed to revert back to a previous breakpoint for a component. For example, if it takes up 50% of the screen, I want it displayed in the small mode. But at a certain screen size, it becomes full-width. In other words, the mode should change one way or the other when there’s a resize event.

Showing three versions of a call-to-action components with nested components within it.

I’ve also been in situations where the same component is used in different modes on different pages. This isn’t something that frameworks like Bootstrap and Tailwind can do, and using media queries to pull it off would be a nightmare. (You can still use those frameworks using this technique, just without the need for the responsive classes they provide.)

We could use a media query that only applies to middle sized screens, but this doesn’t solve the issue with varying props based on screen width. Thankfully, the approach we’re covering can solve that. We can modify the previous code to allow for a custom mode per breakpoint by passing it through an array, with the first item in the array being the smallest screen size.

<CTA :custom-mode="['compact', 'default', 'compact']" />

First, let’s update the props that the <CTA /> component can accept:

props: {   displayMode: {     type: String,     default: "default"   },   customMode: {     type: [Boolean, Array],     default: false   }, }

We can then add the following to generate to correct mode:

import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  // ...  setup(props) {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } },

This is taking the mode from the array based on the current breakpoint, and defaults to the displayMode if one isn’t found. Then we can use mode instead to style the component.

Extraction for reusability

Many of these methods can be extracted into additional composables and mixins that can be reuseD with other components.

Extracting computed mode

The logic for returning the correct mode can be extracted into a composable:

// composables/useResponsive.js  import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  export const useResponsive = (props) => {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } }

Extracting props

In Vue 2, we could repeat props was by using mixins, but there are noticeable drawbacks. Vue 3 allows us to merge these with other props using the same composable. There’s a small caveat with this, as IDEs seem unable to recognize props for autocompletion using this method. If this is too annoying, you can use a mixin instead.

Optionally, we can also pass custom validation to make sure we’re using the modes only available to each component, where the first value passed through to the validator is the default.

// composables/useResponsive.js  // ...  export const withResponsiveProps = (validation, props) => {   return {     displayMode: {       type: String,       default: validation[0],       validator: function (value) {         return validation.indexOf(value) !== -1       }     },     customMode: {       type: [Boolean, Array],       default: false,       validator: function (value) {         return value ? value.every(mode => validation.includes(mode)) : true       }     },     ...props   } }

Now let’s move the logic out and import these instead:

// components/CTA.vue  import Btn from "@/components/ui/Btn"; import { useResponsive, withResponsiveProps } from "@/composables/useResponsive";  export default {   name: "CTA",   components: { Btn },   props: withResponsiveProps(['default 'compact'], {     extraPropExample: {       type: String,     },   }),      setup(props) {     const { mode } = useResponsive(props)     return { mode }   } }

Conclusion

Creating a design system of reusable and responsive components is challenging and prone to inconsistencies. Plus, we saw how easy it is to wind up with a load of duplicated code. There’s a fine balance when it comes to creating components that not only work in many contexts, but play well with other components when they’re combined.

I’m sure you’ve come across this sort of situation in your own work. Using these methods can reduce the problem and hopefully make the UI more stable, reusable, maintainable, and easy to use.


Avoiding the Pitfalls of Nested Components in a Design System originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,

​​Avoiding those dang cannot read property of undefined errors

​​​​Uncaught TypeError: Cannot read property 'foo' of undefined.​ The dreaded error we all hit at some point in JavaScript development. Could be an empty state from an API that returns differently than you expected. Could be something else. We don’t know because the error itself is so general and broad.

​​I recently had an issue where certain environment variables weren’t being pulled in for one reason or another, causing all sorts of funkiness with that error staring me in the face. Whatever the cause, it can be a disastrous error if it’s left unaccounted for, so how can we prevent it in the first place?

​​Let’s figure it out.

​​Utility library

​​If you are already using a utility library in your project, there is a good chance that it includes a function for preventing this error. _.get​ in lodash​ (docs) or R.path in Ramda​ (docs) allow accessing the object safely.
​​
​​If you are already using a utility library, this is likely the simplest solution. If you are not using a utility library, read on!

​​

Short-circuiting with &&

​​​​One interesting fact about logical operators in JavaScript is that they don’t always return a boolean. According to the spec, “the value produced by a &&​ or ||​ operator is not necessarily of type Boolean. The value produced will always be the value of one of the two operand expressions.”
​​
​​​​In the case of the &&​ operator, the first expression will be used if it a “falsy” value. Otherwise, the second expression will be used. This means that the expression 0 && 1​ will be evaluated as 0​ (a falsy value), and the expression 2 && 3​ will be evaluated as 3​. If multiple &&​ expressions are chained together, they will evaluate to either the first falsy value or the last value. For example, 1 && 2 && 3 && null && 4​ will evaluate to null​, and 1 && 2 && 3​ will evaluate to 3​.

​​​​How is this useful for safely accessing nested object properties? Logical operators in JavaScript will “short-circuit.” In this case of &&​, this means that the expression will cease moving forward after it reaches its first falsy value.

​​​​

​​const foo = false && destroyAllHumans(); ​​console.log(foo); // false, and humanity is safe

​​In this example, destroyAllHumans is never called because the &&​ operand stopped all evaluation after false​.

​​This can be used to safely access nested properties.

​​

​​const meals = { ​​  breakfast: null, // I skipped the most important meal of the day! :( ​​  lunch: { ​​    protein: 'Chicken', ​​    greens: 'Spinach', ​​  }, ​​  dinner: { ​​    protein: 'Soy', ​​    greens: 'Kale', ​​  }, ​​}; ​​ ​​const breakfastProtein = meals.breakfast && meals.breakfast.protein; // null ​​const lunchProtein = meals.lunch && meals.lunch.protein; // 'Chicken'

​​Aside from its simplicity, one of the main advantages of this approach is its brevity when dealing with small chains. However, when accessing deeper objects, this can be quite verbose.

​​

const favorites = { ​​  video: { ​​    movies: ['Casablanca', 'Citizen Kane', 'Gone With The Wind'], ​​    shows: ['The Simpsons', 'Arrested Development'], ​​    vlogs: null, ​​  }, ​​  audio: { ​​    podcasts: ['Shop Talk Show', 'CodePen Radio'], ​​    audiobooks: null, ​​  }, ​​  reading: null, // Just kidding -- I love to read ​​}; ​​ ​​const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0]; ​​// Casablanca ​​const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0]; ​​// null

​​The more deeply nested an object is, the more unwieldy it gets.

​​
​​

The “Maybe Monad”

​​Oliver Steele came up with this method and goes through it in much more detail in his blog post, “Monads on the Cheap I: The Maybe Monad.” I will attempt to give a brief explanation here.

​​

const favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined ​​const favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined ​​const favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'

​​Similar to the short-circuit example above, this method works by checking if a value is falsy. If it is, it will attempt to access the next property on an empty object. In the example above, favorites.reading​ is null​, so the books​ property is being accessed from an empty object. This will result in an undefined​, so the 0​ will likewise be accessed from an empty array.

​​The advantage of this method over the &&​ method is that it avoids repetition of property names. On deeper objects, this can be quite a significant advantage. The primary disadvantage would be readability — it is not a common pattern, and may take a reader a moment to parse out how it is working.​

​​

​​try/catch

​​​​try...catch​ statements in JavaScript allow another method for safely accessing properties.

​​

try { ​​  console.log(favorites.reading.magazines[0]); ​​} catch (error) { ​​  console.log("No magazines have been favorited."); ​​}

​​Unfortunately, in JavaScript, try...catch​ statements are not expressions. They do not evaluate to a value as they do in some languages. This prevents a concise try​ statement as a way of setting a variable.

​​One option is to use a let​ variable that is defined in the block above the try...catch​.

​​

let favoriteMagazine; ​​try {  ​​  favoriteMagazine = favorites.reading.magazines[0];  ​​} catch (error) {  ​​  favoriteMagazine = null; /* any default can be used */ ​​};

​​Although it’s verbose, this works for setting a single variable (that is, if the mutable variable doesn’t scare you off). However, issues can arise if they’re done in bulk.

​​

let favoriteMagazine, favoriteMovie, favoriteShow; ​​try { ​​  favoriteMovie = favorites.video.movies[0]; ​​  favoriteShow = favorites.video.shows[0]; ​​  favoriteMagazine = favorites.reading.magazines[0]; ​​} catch (error) { ​​  favoriteMagazine = null; ​​  favoriteMovie = null; ​​  favoriteShow = null; ​​}; ​​ ​​console.log(favoriteMovie); // null ​​console.log(favoriteShow); // null ​​console.log(favoriteMagazine); // null

​​If any of the attempts to access the property fails, this will cause all of them to fall back into their defaults.

​​An alternative is to wrap the try...catch​ in a reusable utility function.

​​

const tryFn = (fn, fallback = null) => { ​​  try { ​​    return fn(); ​​  } catch (error) { ​​    return fallback; ​​  } ​​}  ​​ ​​const favoriteBook = tryFn(() => favorites.reading.book[0]); // null ​​const favoriteMovie = tryFn(() => favorites.video.movies[0]); // "Casablanca"

​​By wrapping the access to the object in a function, you can delay the “unsafe” code and pass it into a try...catch​.

​​A major advantage of this method is how natural it is to access the property. As long as properties are wrapped in a function, they are safely accessed. A default value can also be specified in the case of a non-existent path.

​​Merge with a default object

​​
By merging an object with a similarly shaped object of “defaults,” we can ensure that the path that we are trying to access is safe.
​​
​​

const defaults = { ​​  position: "static", ​​  background: "transparent", ​​  border: "none", ​​}; ​​ ​​const settings = { ​​  border: "1px solid blue", ​​}; ​​ ​​const merged = { ...defaults, ...settings }; ​​ ​​console.log(merged);  ​​/* ​​  { ​​    position: "static", ​​    background: "transparent", ​​    border: "1px solid blue" ​​  } ​​*/

​​
​​Careful, though, because the entire nested object can be overwritten rather than a single property.
​​
​​

const defaults = { ​​  font: { ​​    family: "Helvetica", ​​    size: "12px", ​​    style: "normal", ​​  },         ​​  color: "black", ​​}; ​​ ​​const settings = { ​​  font: { ​​    size: "16px", ​​  } ​​}; ​​ ​​const merged = {  ​​  ...defaults,  ​​  ...settings, ​​}; ​​ ​​console.log(merged.font.size); // "16px" ​​console.log(merged.font.style); // undefined

​​Oh no! To fix this, we’ll need to similarly copy each of the nested objects.

​​

const merged = {  ​​  ...defaults,  ​​  ...settings, ​​  font: { ​​    ...defaults.font, ​​    ...settings.font, ​​  }, ​​}; ​​ ​​console.log(merged.font.size); // "16px" ​​console.log(merged.font.style); // "normal"

​​Much better!

​​This pattern is common with plugins or components that accept a large settings object with included defaults.

​​A bonus about this approach is that, by writing a default object, we’re including documentation on how an object should look. Unfortunately, depending on the size and shape of the data, the “merging” can be littered with copying each nested object.

​​​

The future: optional chaining

​​There is currently a TC39 proposal for a feature called “optional chaining.” This new operator would look like this:

​​console.log(favorites?.video?.shows[0]); // 'The Simpsons' ​​console.log(favorites?.audio?.audiobooks[0]); // undefined

​​The ?.​ operator works by short-circuiting: if the left-hand side of the ?.​ operator evaluates to null​ or undefined​, the entire expression will evaluate to undefined​ and the right-hand side will remain unevaluated.

​​To have a custom default, we can use the ||​ operator in the case of an undefined.

​​

console.log(favorites?.audio?.audiobooks[0] || "The Hobbit");

​​

Which method should you use?

​​The answer, as you might have guessed, is that age-old answer… “it depends.” If the optional chaining operator has been added to the language and has the necessary browser support, it is likely the best option. If you are not from the future, however, there are more considerations to take into account. Are you using a utility library? How deeply nested is your object? Do you need to specify defaults? Different cases may warrant a different approach.

The post ​​Avoiding those dang cannot read property of undefined errors appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]