Build A Reusable UseDetailModal Composable

Alex Johnson
-
Build A Reusable UseDetailModal Composable

Welcome, fellow developers! Today, we're diving into a practical solution to streamline modal management within your wizard step components. Specifically, we'll be crafting a reusable useDetailModal composable. This will significantly reduce code duplication and enhance maintainability, a common pain point in the development of complex user interfaces. Let's get started!

The Problem: Redundant Modal State Management

Let's be honest, repeating the same code snippets over and over is tedious and error-prone. In the context of the wizard step components (StepRace, StepClass, StepBackground, StepSubrace), each component was burdened with the same modal state management logic. This included variables to track modal visibility and the selected item's details. Essentially, each component had to independently manage its modal state.

Imagine the codebase, with multiple instances of similar code blocks across different components. This doesn't just make it harder to read but also makes it difficult to maintain. Any change or bug fix requires you to find and apply the same modifications across all instances, which increases the possibility of missing something and introducing errors. The original code snippet highlighted the issue:

const detailModalOpen = ref(false)
const detailItem = ref<Item | null>(null)

function handleViewDetails(item: Item) {
  detailItem.value = item
  detailModalOpen.value = true
}

function handleCloseModal() {
  detailModalOpen.value = false
  detailItem.value = null
}

This snippet, while functional, is a clear example of code duplication that's begging to be refactored. The detailModalOpen ref controls the modal's visibility, and the detailItem ref stores the details of the selected item. The handleViewDetails function sets the item and opens the modal, while the handleCloseModal function resets these values and closes it. This pattern repeats in various components. The solution? A reusable composable, encapsulating this logic.

The Solution: Introducing useDetailModal<T>()

Our solution is the useDetailModal<T>() composable. This composable uses a generic type parameter <T> to handle different item types. This composable will encapsulate the modal's state and behavior. The objective is to extract the redundant code into a single, reusable unit. This increases code reuse and ensures consistency across your application.

By creating a composable, we can centralize the modal logic, reduce redundancy, and make our code more maintainable. Here's how the useDetailModal<T>() composable would work:

  1. Generic Type Parameter: The <T> signifies a generic type, making our composable adaptable to handle different types of data (e.g., Race, Class, etc.).
  2. Returns: It returns an object with the following properties:
    • open: A reactive boolean that controls the modal's visibility.
    • item: A reactive variable holding the item to be displayed in the modal.
    • show(item: T): A function to show the modal and set the item. This function will take the item to be displayed as an argument, set the item ref, and set open to true.
    • close(): A function to close the modal and clear the item. This function will set open to false and reset the item ref.

This composable will be created in app/composables/useDetailModal.ts.

Implementation Steps

Let's walk through the steps to implement the useDetailModal<T>() composable. This involves creating the composable and integrating it into the wizard step components.

  1. Create the useDetailModal.ts file: Create a new file in your app/composables directory called useDetailModal.ts. This is where the composable will live.

  2. Define the Composable: Inside the file, define the useDetailModal<T>() composable.

    import { ref, type Ref } from 'vue'
    
    export function useDetailModal<T>() {
      const open: Ref<boolean> = ref(false)
      const item: Ref<T | null> = ref(null)
    
      function show(newItem: T) {
        item.value = newItem
        open.value = true
      }
    
      function close() {
        open.value = false
        item.value = null
      }
    
      return { open, item, show, close }
    }
    
  3. Integrate into Wizard Step Components: Import and use the composable within your wizard step components (e.g., StepRace.vue, StepClass.vue, etc.).

    <template>
      <div>
        <button @click="handleViewDetails(race)">View Details</button>
    
        <Modal v-if="detailModal.open" @close="detailModal.close">
          <!-- Display details of detailModal.item -->
          <p>{{ detailModal.item.name }}</p>
        </Modal>
      </div>
    </template>
    
    <script setup lang="ts">
    import { useDetailModal } from '~/composables/useDetailModal'
    
    interface Race {
      name: string;
    }
    
    const detailModal = useDetailModal<Race>()
    
    const race: Race = {
      name: 'Human',
    }
    
    function handleViewDetails(item: Race) {
      detailModal.show(item)
    }
    </script>
    

    In the example above, the component imports the composable, calls it to get the open, item, show, and close properties. The show function is used to open the modal and pass the item to it and the modal is closed with the close function.

  4. Unit Tests: Create unit tests for the composable in tests/composables/useDetailModal.test.ts. This ensures that the composable functions as expected and the unit tests will cover all its functionality.

Acceptance Criteria Revisited

Let's ensure the solution meets all the acceptance criteria defined earlier:

  • Composable Created with Generic Type Parameter: The composable useDetailModal<T>() is created and uses a generic type parameter <T>, which allows it to be reused with different data types.

  • Returns { open, item, show, close }: The composable returns an object with open, item, show, and close properties, as expected. The open and item are reactive properties, and show and close are methods to manipulate the modal state.

  • Unit Tests Written: Unit tests are required to ensure the correct functionality and prevent future regressions. These tests would cover the show and close functions, ensuring that they properly manipulate the open and item reactive variables.

  • TypeScript Types Correct: The composable and its properties are correctly typed using TypeScript, ensuring type safety and code maintainability.

Benefits of this Approach

Creating a useDetailModal composable brings numerous benefits to your development process. Firstly, it reduces code duplication. Instead of repeating the modal state management code in each wizard step component, you now have a single source of truth for this logic. Second, it enhances maintainability. Changes or bug fixes related to modal management only need to be applied in one place. Third, it improves code readability. Components become cleaner and more focused on their primary function - rendering the wizard steps. The approach reduces the complexity of individual components.

Beyond these direct benefits, the composable approach promotes a more modular and reusable codebase. It encourages a clear separation of concerns, making it easier to reason about the application's structure and behavior. It also facilitates easier testing, as you can test the composable's functionality in isolation. This ultimately makes your application more scalable and robust.

Conclusion

By creating a useDetailModal<T>() composable, you've taken a significant step toward a more maintainable, efficient, and readable codebase. This composable reduces code duplication, enhances maintainability, and improves overall code quality. Remember to always strive for code reuse and modularity in your projects. By encapsulating reusable logic into composables, you can keep your components lean and focused on their primary responsibilities, making your projects easier to manage and scale.

For more information on Vue composables, you can check out the official Vue.js documentation. This is a crucial skill for modern Vue.js development!

External Link:

You may also like