vue.jsnuxt.jsvuejs3nuxt3.jsvue-reactivity

How to pass a reactive object with prop values from a component to a composable in vue3 / nuxt3


i have a vue component using the composition API that has some props passed into it.

i would like this component to pass its props into a composable that's a wrapper around useFetch, and i would like the composable to retain reactivity when the component's props change. i.e. i want useFetch to re-run when the props change.

the following works when i explicitly pass the props in as refs to the composable.

component:

<script setup lang="ts">
  const props = defineProps({
    page: {
      default: 1,
      required: false,
      type: Number,
    },
    search: {
      required: true,
      type: String,
    },
  });

  const { data, error, pending } = await useGetData(toRef(props, 'page'), toRef(props, 'search'));
</script>

composable:

export default (page: Ref<number>, search: Ref<string>) => {
  const limit = ref(20);
  const offset = computed(() => {
    const p = page.value || 1;
    return (p > 1) ? ((p - 1) * limit.value) : 0;
  });

  return useLazyFetch<DataIndex>('/api/data', {
    query: {
      limit,
      offset,
      search,
    },
    deep: false,
  });
});

now, i would like to pass the props to my composable as an object instead of an ordered set of arguments. when i do this i seem to lose reactivity on the props - i.e. the query doesn't re-run when the props change in my component.

i've tried various interations of attempting to pass the props in as refs, but have had no luck.

component:

<script setup lang="ts">
  const props = defineProps({
    page: {
      default: 1,
      required: false,
      type: Number,
    },
    search: {
      required: true,
      type: String,
    },
  });
  const dataInput = reactive({
    page: props.page,
    search: props.search,
  });
  const { data, error, pending } = await useGetData(toRefs(dataInput));
</script>

composable:

interface GetDataInput {
  page?: Ref<number>
  search?: Ref<string>
};

export default (input: GetDataInput) => {
  const limit = ref(20);
  const offset = computed(() => {
    const p = input.page?.value || 1;
    return (p > 1) ? ((p - 1) * limit.value) : 0;
  });

  return useLazyFetch<DataIndex>('/api/data', {
    query: {
      limit,
      offset,
      search: input.search,
    },
    deep: false,
  });
});

i assume that i'm losing reactivity in this example because either dataInput is not seeing the prop change, or because the input argument itself is not reactive when passed into the composable.

what am i missing here?


Solution

  • Your problem is using defineProps in the wrong way.

    defineProps will return a reactive object (a Proxy), means props is now reactive. Since you declare a new reactive with your props.x. dataInput is a new reactive object that is initialized with value of props.x (just value initialization, no reactivity here)

    That's why when props is changed, dataInput is kept intact cause they are not related or connected. Try to use toRefs(props) instead

    <script setup lang="ts">
      const props = defineProps({
        page: {
          default: 1,
          required: false,
          type: Number,
        },
        search: {
          required: true,
          type: String,
        },
      });
      //const dataInput = reactive({
      //  page: props.page,
      //  search: props.search,
      //});
      const { data, error, pending } = await useGetData(toRefs(props));
    </script>
    

    And for some reason, if you still want to keep dataInput, try this instead

    <script setup lang="ts">
      const props = defineProps({
        page: {
          default: 1,
          required: false,
          type: Number,
        },
        search: {
          required: true,
          type: String,
        },
      });
      const dataInput = reactive(toRefs(props));
      const { data, error, pending } = await useGetData(toRefs(dataInput));
    </script>