0

I'm trying to create a custom hook that will keep the text on one line, this is what I've got so far:

import { useEffect, useState, useRef } from "react";

export default function useOneLineText(initialFontSize, text) {
  const containerRef = useRef(null);
  const [adjustedFontSize, setAdjustedFontSize] = useState(initialFontSize);

  useEffect(() => {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const containerWidth = containerRef.current.getBoundingClientRect().width;
    let currentFontSize = initialFontSize;

    do {
      ctx.font = `${currentFontSize}px "Lato"`;
      currentFontSize -= 1;
    } while (containerWidth < ctx.measureText(text).width);

    setAdjustedFontSize(currentFontSize);
  }, []);

  return { containerRef, adjustedFontSize, text };
}

And the usage:

import React, { useState } from "react";
import "./style.css";
import useOneLineText from "./useOneLineText";

const initialFontSize = 32;

export default function App() {
  const [shortText, setShortText] = useState("Hello Peaceful World!");
  const { containerRef, adjustedFontSize, text } = useOneLineText(
    initialFontSize,
    shortText
  );

  return (
    <div>
      <h1
        ref={containerRef}
        style={{ width: "100px", fontSize: `${adjustedFontSize}px` }}
      >
        {text}
      </h1>

      <h1 style={{ width: "100px", fontSize: `${initialFontSize}px` }}>
        Hello Cruel World!
      </h1>

      <button onClick={() => setShortText("Hello World!")}>Remove Peace</button>
    </div>
  );
}

I was wondering why is my font not updating when trying to change the text with the Remove Peace button. I'm also not sure if looping inside useEffect and decreasing the font size by 1 is the best way to go, is it possible to calculate the adjusted font size without the loop? Here is my working example.

htmn
  • 1,505
  • 2
  • 15
  • 26

1 Answers1

0

Well, your code already works as it seems. From your example, the initial font size of 32px changes to a 10px on mount. Also, your effect only runs on mount. Without removing the word, it would also already satisfy the containerWidth < ctx.measureText(text).width.

If you want the effect to happen when the containerWidth or textWidth changes, then you can add more states and add them on your useEffect dependency list.

const [container, setContainerState] = useState(null);
const [textWidth, setTextWidth] = useState(null);

useEffect(() => {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  const refWidth = containerRef.current.getBoundingClientRect().width;
  let currentFontSize = initialFontSize;

  if (
    textWidth !== ctx.measureText(text).width ||
    containerWidth !== refWidth
  ) { 
    do {
      ctx.font = `${currentFontSize}px "Lato"`;
      currentFontSize -= 1;
    } while (refWidth < ctx.measureText(text).width);
    setContainerWidth(containerWidth);
    setTextWidth(textWidth);
    setAdjustedFontSize(currentFontSize);
  }
}, [textWidth, containerWidth]);

For a faster adjusting of font size, you can check this answer out from a jquery question that uses binary search to help you find the right width quicker?

Auto-size dynamic text

Leomar Amiel
  • 469
  • 3
  • 13