templatescanvasvuejs3vue-composition-apirefs

Vue3 TypeError: template ref.value is null


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.


Solution

  • 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>