Search code examples
vue.jsvuejs2vue-componentvuejs-slots

Vue 2 Infinite Loop Across Parent and Child Component Using Slots


I need to call a method in a child from a parent component. However, something I'm doing in the child component appears to be causing an infinite loop. I've tried looking at other questions, and while they appear to be getting at a similar problem, I can't apply their exact patterns to the issue I'm facing, it appears to be different. I'm just not sure what I'm looking at.

I have 2 components, one called ToggleButtons and another that has been simplified down to a button. Here is ToggleButtons:

<template>
    <div role="list">
        <div role="listitem">
            <slot name="left" :is-selected="leftSelected" :toggleLeft="toggle('left')" />
        </div>
        <div role="listitem">
            <slot name="right" :is-selected="rightSelected" :toggleRight="toggle('right')" />
        </div>
    </div>
</template>

<script>

    export default {
        props: {
            leftSelectedInitially: {
                type: Boolean,
                default: true,
            }
        },

        data() {
            return {
                leftSelected: true,
                rightSelected: false,
            }
        },

        beforeMount() {
            this.leftSelected = this.leftSelectedInitially;
            this.rightSelected = !this.leftSelectedInitially;
        },

        methods: {
            toggle(override) {
                this.leftSelected = override == 'left';
                this.rightSelected = override == 'right';
            }
        }

    }
</script>

and here is its implementation with the button:

<ToggleButtons ref="tb">
    <template v-slot:left="{
        isSelected,
    }">
        <button
            class="button"
            :class="{ secondary: !isSelected }"
            :aria-pressed="isSelected"
            :togglable="true"
            v-text="'left'"
            @click="toggle('left')"
        />
    </template>
    <template v-slot:right="{
        isSelected,
    }">
        <button
            class="button"
            :class="{ secondary: !isSelected }"
            :aria-pressed="isSelected"
            :togglable="true"
            v-text="'right'"
            @click="toggle('right')"
        />
    </template>
</ToggleButtons>

where the toggle method is:

toggle(direction) {
    this.$refs.tb.toggle(direction);
},

As you may already be able to see from leftovers in the code, i've tried various patterns previously, including passing a toggle method through the v-slot. All of these result in the same "you have created an infinite loop" message.

I'm wondering if it's because the method is calling toggle while it's trying to render. I'm not sure if that would actually cause an infinite loop or not. My main problem here is that I don't understand where this loop is coming from. My main goal at this point is to understand what's going wrong, so if it happens again, I'll be able to see it, even if the fix is to just figure out a more simple way of doing this.


Solution

  • The following bindings to the toggle function do not make any sense to me:

    :toggleLeft="toggle('left')"
    :toggleRight="toggle('right')
    

    Since the function does not return any value, it's simply wrong.

    The both bindings cause the function to be called endlessly with toggle('left') and toggle('right')

    Just add console.log(direction) to the toggle function to see what's going on.

    JS Console output

    If you want to get an advice about the right solution, then please describe what you are trying to achieve.

    Vue.component('toggle-buttons',{
      props: {
        leftSelectedInitially: {
            type: Boolean,
            default: true,
        }
      },
      data() {
          return {
              leftSelected: true,
              rightSelected: false,
          }
      },
      beforeMount() {
          //this.leftSelected = this.leftSelectedInitially;
          //this.rightSelected = !this.leftSelectedInitially;
      },
      methods: {
          toggle(override) {
              console.log(`override: ${override}`)
              this.leftSelected = override == 'left';
              this.rightSelected = override == 'right';
          }
      },
      template: `
    <div role="list">
      <div role="listitem">
          <slot name="left" :is-selected="leftSelected" :toggleLeft="toggle('left')" />
      </div>
      <div role="listitem">
          <slot name="right" :is-selected="rightSelected" :toggleRight="toggle('right')" />
      </div>
    </div>
    `
    });
    
    new Vue({
      el:'#app',
      methods: {
      toggle(direction) {
        console.log(`direction: ${direction}`)
        this.$refs.tb.toggle(direction);
        }
      }
    })
    #app { line-height: 2; }
    [v-cloak] { display: none; }
    <div id="app">
    <toggle-buttons ref="tb">
        <template v-slot:left="{ isSelected }">
            <button
                class="button"
                :class="{ secondary: !isSelected }"
                :aria-pressed="isSelected"
                :togglable="true"
                v-text="'left'"
                @click="toggle('left')"
            />
        </template>
        <template v-slot:right="{ isSelected }">
            <button
                class="button"
                :class="{ secondary: !isSelected }"
                :aria-pressed="isSelected"
                :togglable="true"
                v-text="'right'"
                @click="toggle('right')"
            />
        </template>
    </toggle-buttons>
    </div>
    <script src="https://unpkg.com/vue@2/dist/vue.min.js"></script>