Issue
I am trying to render a text from a text input on an SVG. I run into the problem that the text is not rendered, since the font gets loaded then the element gets destroyed and the loaded font is empty again.
The text has to have an own font for every line so I put everything together into one object
const textObject = {
font: false, //the opentype.js font object gets loaded into here
fontFamily: false, // the font family gets set here
fontSize: 100,
text: "", //the actual text string to generate the path from
color: "#000000"
}
App.js is setting up the DOM like this
return (
<div className="App">
<ContextProvider setLineNum={e => handleLineNum(e)}>
{fontSelect}
<TextBox />
<TextOutput />
</ContextProvider>
</div>
);
In context provider I have a function on the context that splits the text from the textbox into lines and assembles them into the textObject
const editTheText = (t) => {
const textLines = t.split(/\r?\n/);
//remove last element of text array if empty
if (textLines[textLines.length - 1].text === "" && textLines.length > 1) {
textLines.splice(-1);
};
let textArray = [];
textLines.forEach((line, i) => {
textArray[i] = { ...textObject, text: line };
setLines(i + 1);
});
setTheText(textArray);
props.setLineNum(textArray.length);
console.log("editTheText", textArray);
}
The font object gets loaded into the text each time a new <FontSelect>
is generated.
In FontSelect:
useEffect(() => {
if (fontSelectRef.current) return; //run only once on mount
fontSelectRef.current = true;
setFont(initialFont, props.id); //Send Font name and line num (id of FontSelect component) to context
});
In the ContextProvider:
const setFont = (fontFamily, lineNum) => {
console.log("setFont", fontFamily, "lineNum", lineNum, theText);
if (theText[lineNum].fontFamily !== fontFamily) {
console.log("New Font:", fontFamily);
opentype.load('ttf/' + fontFamily + '.ttf', (err, font) => {
if (err) {
alert('Font could not be loaded: ' + err);
} else {
console.log("loaded Font:", fontFamily);
setTheText(prev => {
prev[lineNum].font = font;
prev[lineNum].fontFamily = fontFamily;
return [...prev];
});
}
});
}
}
This useEffect runs showing filled font, gets destroyed, runs again
useEffect(()=> {
console.log('theText effect is running', theText);
return () => console.log('theText effect is destroying', theText);
}, [theText]);
Now when I enter a text into the textbox, "editTheText" runs, sets theText.text = "a" but font is false again.
Why is theText destroyed all the time? Why is it running again but without the loaded text?
Edit CodeSandbox: https://codesandbox.io/s/fervent-ptolemy-ckfg62 (ttf url is not working, tried several, doesn't work. It works however when I load them locally as you can see on the screenshot)
Solution
So there are a few problems, the first is inside the ContextProvider and the second is inside the FontSelect
function.
The problem with the ContextProvider
:
const textObject = {
font: false,
fontFamily: false,
fontSize: 100,
text: "",
color: "#000000"
};
const [theText, setTheText] = useState([textObject]);
const [lines, setLineNum] = useState(1);
//const [fontList, setFontList] = useState({});
const editTheText = (t) => {
const textLines = t.split(/\r?\n/);
//remove last element of text array if empty
if (textLines[textLines.length - 1].text === "" && textLines.length > 1) {
textLines.splice(-1);
}
let textArray = [];
textLines.forEach((line, i) => {
textArray[i] = { ...textObject, text: line }; // problem here
setLines(i + 1);
});
setTheText(textArray);
props.setLineNum(textArray.length);
console.log("editTheText", textArray);
};
The problem is that you are spreading the default textObject instead of the state from theText
. The state in theText
contains the selected font. So basically everytime a user types something, you are reseting theText
state to it's default value.
Here is the proper function:
const editTheText = (t) => {
const textLines = t.split(/\r?\n/);
//remove last element of text array if empty
if (textLines[textLines.length - 1].text === "" && textLines.length > 1) {
textLines.splice(-1);
}
let textArray = [];
textLines.forEach((line, i) => {
textArray[i] = { ...theText[0], text: line }; // notice how we're spreading `theText` object
setLines(i + 1);
});
setLineNum(textArray.length);
};
The second problem is when you are setting the font inside FontSelect
.
useEffect(() => {
if (fontSelectRef.current) return; //run only once on mount
fontSelectRef.current = true;
setFont(initialFont, props.id);
});
You have a useEffect that has no dependancy array which means that it will run on every render even when nothing has changed. You'll want to only set it when props.id
changes which will only happen when it's initially rendered.
useEffect(() => {
setFont(initialFont, props.id);
}, [props.id]);
Answered By - user
Answer Checked By - - Clifford M. (ReactFix Volunteer)