20

how can I clean my console from the following error:

enter image description here

TypeError: ref.value is null

The error comes only with a resize event. Each time I resize the window, I render the chart. So the error message appears again and again. The documentation shows that the template ref is also initialized with a null value (Source). So I have to do something after initialization, right?

Here is my code:

<template>
  <canvas
    ref="chartRef"
  />
</template>
<script setup>
// ...
// on resize
export const chartRef = ref(null)
export function createChart () {
  const ctx = chartRef.value.getContext('2d')
  if (ctx !== null) { // fix me
    getDimensions()
    drawChart(ctx)
  }
}
// ...
</script>

How can I clean up my console so that the error message no longer appears? Thx.

2
  • Once onMounted and then I add an eventlistener for each resize, with a debounce of 250ms. Commented Jan 16, 2021 at 21:52
  • 1
    No, the script setup is essential in my project. Can't do it without it. Commented Jan 16, 2021 at 22:04

7 Answers 7

35

I am a bit too late, but I faced the same issue with Vue 3. I solved juste by returning the reference :

<template>
  <input ref="myinput">
</template>


<script>
import { onMounted, ref } from 'vue'

export default {
  setup() {

    const myinput = ref(null) // Assign dom object reference to "myinput" variable

    onMounted(() => {
      console.log(myinput.value) // Log a DOM object in console
    })

    return { myinput } // WILL NOT WORK WITHOUT THIS
  }
}
</script>
Sign up to request clarification or add additional context in comments.

3 Comments

Hi, thanks for your answer but it is the same as the documentation for the Composition API says. I am using the experimental script setup version in my case unfortunately.
That you need to return it solved it for me as well!
When using the script setup in combination with data that is getting fetched, I had to wait until the data was available. I.e. I only accessed the ref once it was not null anymore.
20

If you're seeing the yourRef.value as null then maybe the element you're trying to get the ref to is inside a false v-if. Vue will not instantiate the ref until the element is actually required.

2 Comments

Thank you, I was confused when the element seems inconsistent in value
This was the problem I had. Solved it by using :class with Bootstraps d-none and d-block instead of using v-if.
7

Option A

wrap it in a try...catch

Option 2

Using a watch


I've found the best way to do it is to use a watch

Here is an example of a function that can be reused between multiple components. We can define a function that generates the canvas reference that can then be passed to the component - canvasRef .

const withCanvasRef = () => {
  let onMountCallback = null;
  const onMount = callback => {
    onMountCallback = callback;
  };
  const canvasRef = ref(null);

  watch(canvasRef, (element, prevElement) => {
    if (element instanceof HTMLCanvasElement) {
      canvasRef.value = element;
      if (onMountCallback && prevElement === null) onMountCallback(canvasRef);
    } else {
      ctxRef.value = null;
    }
  });
  return {
    canvasRef,
    onMount
  };
};

We can then get the canvasRef in the component and pass it to the <canvas> element. We can also use the onMounted hook that the function returns to handle initial render.

app.component("my-line-chart", {
  setup: props => {
    const { canvasRef, onMount } = withCanvasRef();

    const draw = () => {
      // stuff here,
      // use a check for canvasRef.value if you have conditional rendering
    };

    // on resize
    window.addEventListener("resize", () => draw());

    // on canvas mount
    onMount(() => draw());

    return { canvasRef };
  },
  template: `<div><canvas ref="canvasRef"/></div>`
});

See example 👇 for example showing this in action. Hopefully you can see the benefit of using Composition API as a solution for better code reuse and organization. (Even though some aspects of it seem a bit more laborious, like having to define a watch for props manually)

const app = Vue.createApp({
  setup() {
    const someData = Vue.ref(null);
    let t = null;

    const numPts = 20;
    const generateData = () => {
      const d = [];
      for (let i = 0; i < numPts; i++) {
        d.push(Math.random());
      }

      if (someData.value == null) {
        someData.value = [...d];
      } else {
        const ref = [...someData.value];
        let nMax = 80;
        let n = nMax;
        t !== null && clearInterval(t);

        t = setInterval(() => {
          n = n -= 1;
          n <= 0 && clearInterval(t);
          const d2 = [];
          for (let i = 0; i < numPts; i++) {
            //d2.push(lerp(d[i],ref[i], n/nMax))
            d2.push(ease(d[i], ref[i], n / nMax));
          }
          someData.value = [...d2];
        }, 5);
      }
    };
    generateData();
    return { someData, generateData };
  }
});

const withCanvasRef = () => {
  let onMountCallback = null;
  const onMount = callback => {
    onMountCallback = callback;
  };
  const canvasRef = Vue.ref(null);

  Vue.watch(canvasRef, (element, prevElement) => {
    if (element instanceof HTMLCanvasElement) {
      canvasRef.value = element;
      if (onMountCallback && prevElement === null) onMountCallback(canvasRef);
    } else {
      ctxRef.value = null;
    }
  });
  return {
    canvasRef,
    onMount
  };
};

const drawBarGraph = (canvas, data) => {
  const width = canvas.width;
  const height = Math.min(window.innerHeight, 200);
  const ctx = canvas.getContext("2d");

  const col1 = [229, 176, 84];
  const col2 = [202, 78, 106];

  const len = data.length;
  const mx = 10;
  const my = 10;
  const p = 4;
  const bw = (width - mx * 2) / len;

  const x = i => bw * i + p / 2 + mx;
  const w = () => bw - p;
  const h = num => (height - my * 2) * num;
  const y = num => (height - my * 2) * (1 - num) + my;
  const col = i => {
    const r = lerp(col1[0], col2[0], i / len);
    const g = lerp(col1[1], col2[1], i / len);
    const b = lerp(col1[2], col2[2], i / len);
    return `rgb(${[r, g, b]})`;
  };

  data.forEach((num, i) => {
    ctx.fillStyle = col(i);
    ctx.fillRect(x(i), y(num), w(), h(num));
  });
};

const drawLineGraph = (canvas, data) => {
  const width = canvas.width;
  const height = Math.min(window.innerHeight, 200);
  const ctx = canvas.getContext("2d");

  const col1 = [229, 176, 84];
  const col2 = [202, 78, 106];

  const len = data.length;
  const mx = 10;
  const my = 10;
  const p = 4;
  const bw = (width - mx * 2) / len;

  const x = i => bw * i + p / 2 + mx + bw / 2;
  const y = num => (height - my * 2) * (1 - num) + my;
  const r = 2;

  const col = i => {
    const r = lerp(col1[0], col2[0], i / len);
    const g = lerp(col1[1], col2[1], i / len);
    const b = lerp(col1[2], col2[2], i / len);
    return `rgb(${[r, g, b]})`;
  };

  ctx.lineWidth = 0.2;
  ctx.strokeStyle = "black";
  ctx.beginPath();
  data.forEach((num, i) => {
    i == 0 && ctx.moveTo(x(i), y(num));
    i > 0 && ctx.lineTo(x(i), y(num));
  });
  ctx.stroke();
  ctx.closePath();

  data.forEach((num, i) => {
    ctx.beginPath();
    ctx.fillStyle = col(i);
    ctx.arc(x(i), y(num), r, 0, 2 * Math.PI);
    ctx.fill();
  });
};

const drawSomething = canvas => {
  canvas.width = window.innerWidth / 2 - 5;
  canvas.height = Math.min(window.innerHeight, 200);
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "rgb(255 241 236)";
  ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
};

app.component("my-bar-chart", {
  props: ["data"],
  setup: props => {
    const { canvasRef, onMount } = withCanvasRef();

    const draw = () => {
      if (canvasRef.value) {
        drawSomething(canvasRef.value);
        drawBarGraph(canvasRef.value, props.data);
      }
    };

    // on resize
    window.addEventListener("resize", () => draw());

    // on data change
    Vue.watch(
      () => props.data,
      () => draw()
    );

    // on canvas mount
    onMount(() => draw());

    return { canvasRef };
  },
  template: `<div><canvas ref="canvasRef"/></div>`
});

app.component("my-line-chart", {
  props: ["data"],
  setup: props => {
    const { canvasRef, onMount } = withCanvasRef();

    const draw = () => {
      if (canvasRef.value) {
        drawSomething(canvasRef.value);
        drawLineGraph(canvasRef.value, props.data);
      }
    };

    // on resize
    window.addEventListener("resize", () => draw());

    // on data change
    Vue.watch(
      () => props.data,
      () => draw()
    );

    // on canvas mount
    onMount(() => draw());

    return { canvasRef };
  },
  template: `<div><canvas ref="canvasRef"/></div>`
});

app.mount("#app");

const lerp = (start, end, amt) => (1 - amt) * start + amt * end;
const ease = (start, end, amt) => {
  return lerp(start, end, Math.sin(amt * Math.PI * 0.5));
};
body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}
.chart {
  display: inline-block;
  margin-right: 4px;
}
<script src="https://unpkg.com/vue@next/dist/vue.global.prod.js"></script>

<div id="app">
  <button @click="generateData">Scramble</button>
  <div>
    <my-bar-chart class="chart" :data="someData"></my-bar-chart>
    <my-line-chart class="chart" :data="someData"></my-line-chart>
  </div>
</div>

1 Comment

thanks, it looks really promising. I'll will give it a try. I'm sorry if it's gonna take a little.
5

Scratched my head for a while over this, but basically, when using <script setup>, the public instance of the component that is exposed (when retrieved from template refs) won't expose any of the bindings declared inside.

If you want them exposed, use defineExpose().

<script setup>
    import { ref } from 'vue'

    const a = 1
    const b = ref(2)

    defineExpose({
        a,
        b
    })
</script>

It's a compiler macro, so you don't need to import it.

1 Comment

how does this work with the resize problem he(and I) are struggling with? I tried to add it after the declaration like your show, but it still gives the error when I try to use the 'b' for a resize using: " onMounted (() => { function onWindowResize() { window.addEventListener("resize", () => { b.value.textContent = 'resize' }) } onWindowResize() })"
1

I'm a bit late too, but considering it's for Vue 3 and it's still 2021 here is my solution for those who are not using Composition API:

In one of sub-components, I had something like this:

<div ref='popupMenu'>
... some content here ...
</div>

As expected, this.$refs.popupMenu was set in mounted and activated callbacks, however, for whatever reason, when I attempted to approach it from a window scroll listener, it was set to null. I've consoled out entire component to see what's going on, and I could see $refs.popupMenu was null.

Not sure if it was something I did, or maybe my misunderstanding on how mount/activate works when you have parent component also doing re-rendering of it's subcomponents, but what worked out for me was:

Template

<div :ref='storeReference'>
.... popup content here ...
</div>

Data

data() {
    popupMenu: null
}

Methods

methods: {
    storeReference(e) {
        if(e) {
            this.popupMenu = e;
        }
    }
    clearReference(e) {
        this.popupMenu = null;
    }
}

This would ensure I keep the reference in all times, and worked well in my case.

Would have a method clearReference when control is unmounted or deactivated that would set this.popupMenu = null so it does not keep it in the memory.

Comments

1

In my case, I was trying to use the reference in the onBeforeMount function. Obviously, that doesn't work, I can only use it once the component is rendered because that's when the reference is set.

Comments

1

I ran into same error message when trying to reference a component to use one of its inner properties in a computed function. The ref wasn't ready to be used during the mount by the computed evaluation. Fix was adding the ? to the .value to treat null ref value as false.

<template>
   <ChartComponent ref="chartRef" />
   <button v-if="showButton">Label</button>
</template>

<script setup lang="ts">
   const chartRef = ref<any>(null)
   const showButton = computed(() => chartRef.value?.chartProperty)
</script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.