- I am using remotion/react and Zustand to create a captions editor. I have a text box which I want to reflect the video component. However, the text on the video being displayed (and console.log'ed) are always one character (or render?) behind.
For example, when the box says 'textt' the video component will display 'text'. If the box is 'texttt' the component will be 'textt'. It goes backwards to. If we turn 'texttt' -> 'textt', the component will reach 'texttt'. Why is this happening?
The second issue is tied to the first. A new CaptionBox should appear when a new caption object is added. However, it only happens when we somehow force a re-render, by clicking the pause/play button of the player (which is connected to our captions object).
When we click the pause/play button, the text displayed in the Player component is reset to the initial Zustand state, no longer reflecting our CaptionBoxes. It all also resets to initial state if we refresh the page. How to prevent that?
source code: https://github.com/polooner/makeklips/tree/main
function CaptionBox({ id, timeFrom, timeTo, text }: KeyFrame) {
console.log(text);
const [value, setValue] = useState(text);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// const [cursorPosition, setCursorPosition] = useState(0);
const { getKeyFrames, updateKeyFrame, setKeyFrames } = useMakeKlips();
// const handleCursorChange = (e: KeyboardEvent) => {
// const input = textareaRef.current;
// if (input) {
// const cursorPosition = input.selectionStart;
// const textBeforeCursor = input.value.substring(0, cursorPosition);
// const lineCount = textBeforeCursor.split('\n').length;
// setCursorPosition(lineCount);
// console.log(cursorPosition);
// }
// };
//TODO: on backspace, pop the sentence and append to first if last character is the newline sequence (\r\n)
//TODO: on enter, sentence into another array element
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
//FIXME: text and other elements are a render behind
console.log(e.target.value);
setValue(e.target.value);
console.log(e.target.value.includes('\n\n'));
// if pushed a double new line, just cut at the end of double \n and create a new keyframe
if (e.target.value.includes('\n\n')) {
//FIXME: we see a new object in console but no new CaptionBox appears
const valueCutIntoArray = e.target.value.split('\n\n');
setValue(valueCutIntoArray[0]);
updateKeyFrame(String(id), {
text: valueCutIntoArray[0],
timeFrom,
timeTo,
id,
});
//TODO: prevent user from infinitely pressing Enter
console.log(valueCutIntoArray[0].length + valueCutIntoArray[1].length);
setKeyFrames((prevKeyFrames) => {
return prevKeyFrames.concat({
id: prevKeyFrames.length + 1,
text: valueCutIntoArray[1],
timeFrom: 160,
timeTo: 180,
});
});
console.log(valueCutIntoArray);
} else {
updateKeyFrame(String(id), {
text: value,
timeFrom,
timeTo,
id,
});
}
const frames = getKeyFrames();
console.log(frames);
};
return (
<div className='p-4 hover:bg-zinc-900 w-full rounded-2xl flex justify-between items-center !bg-zinc-900 flex-row'>
<div style={{ direction: 'ltr' }} className='w-full'>
<span
suppressHydrationWarning
className='text-xs px-2 font-sans block text-slate-500 font-display'
>
{timeFrom}-{timeTo}
</span>
{/* TODO: stretch textarea infinitely */}
<textarea
ref={textareaRef}
className='resize-none w-full text-white bg-transparent overflow-hidden p-2 font-inherit'
value={value}
onChange={(e) => handleChange(e)}
// onKeyUp={handleCursorChange}
// onClick={handleCursorChange}
// onSelect={handleCursorChange}
/>
</div>
</div>
);
}
export default Captions;
Zustand state logic (hooks):
import { createWithEqualityFn } from 'zustand/traditional';
// import { applyNodeChanges } from '../utils/';
import getInitialState from './initialState';
import type { KeyFrame, MakeKlipsState } from '@/types/store';
const createMKStore = ({ keyFrames }: { keyFrames?: KeyFrame[] }) =>
createWithEqualityFn<MakeKlipsState>(
(set, get) => ({
...getInitialState({ keyFrames }),
setKeyFrames: (keyFrames: KeyFrame[]) => {
//TODO: implement lookup for perf
// const { frameLookup } = get();
// updateFrameLookup(frameLookup, keyFrames);
set({ keyFrames });
},
updateFrame: (params) => {
const { keyFrames } = get();
const frameToUpdateIdx = keyFrames.findIndex((frame) => {
frame.id === params.id;
});
//TODO: make params optional
if (frameToUpdateIdx != -1) {
if (params.id) keyFrames[frameToUpdateIdx].id = params.id;
if (params.text) keyFrames[frameToUpdateIdx].text = params.text;
if (params.timeFrom)
keyFrames[frameToUpdateIdx].timeFrom = params.timeFrom;
if (params.timeTo) keyFrames[frameToUpdateIdx].timeTo = params.timeTo;
get().triggerKeyFramesChanges(keyFrames);
} else {
console.log('Could not find the frame to update');
}
},
triggerKeyFramesChanges: (keyFrames) => {
set({ keyFrames });
// onNodesChange?.(changes);
},
}),
Object.is
);
export { createMKStore };