I am trying to make a typing effect, and its a very simple logic,,,but still i am not able to understand that why the 'e' in hello is always missing everything else is working fine. have not made the blinking cursor yet !!!!
CODE:
import { useState } from "react";
export function Type() {
let str = "hello my name is prateek"
const [char, setChar] = useState("");
function type() {
let i = 0;
let id = setInterval(() => {
setChar(prev => prev + str[i]);
//console.log(i,"i")
// console.log(str[i])
i++;
if (i === str.length - 1) {
//console.log("hello")
clearInterval(id)
}
}, 1000);
}
return (<div>
<h1>{char}</h1>
<button onClick={type}>Type</button>
</div>)
}
OUTPUT
hllo my name is prateek
I think you might have an asynchronous race condition:
Async callbacks go into an event queue.
The delay argument is like a minimum, see MDN for more on reasons delays can take longer
Though I could use help finding the source for dispatcher (source code), I strongly suspect React Hooks like useState use async callbacks and the event queue.
Assuming useState is an async callback, I suspect it has no delay.
One suggestion: use a loop and setTimeout() with 1000*i delay. The loop ensures you'll add each letter, the delay will add each letter ~1s apart.
Another suggestion: Dan Abramov did a really interesting blog post digging into this: Making setInterval declarative with React Hooks (2019). His solution explores a custom hook he writes, useInterval (not part of React Hooks API), and explains what's happening with React render cycles and "sliding delay".
const { useState } = React;
function Type(){
let str = "hello my name is prateek"
const [char,setChar] = useState("");
function type(){
let i = 0;
const id = setInterval(()=>{
setChar(prev=>prev+str[i]);
//console.log(i,"i")
// console.log(str[i])
i++;
if(i === str.length-1){
//console.log("hello")
clearInterval(id)
}
},1000);
}
return <div>
<h2>{char}</h2>
<button onClick={type}>Type</button>
</div>
}
function TypeWorking(){
let str = "hello my name is prateek"
const [char,setChar] = useState("");
function type(){
for(let i=0; i<str.length; i++) {
setTimeout(()=> setChar(prev=>prev+str[i]), 1000*(i+1));
}
}
return <div>
<h2>{char}</h2>
<button onClick={type}>Type</button>
</div>
}
ReactDOM.createRoot(
document.getElementById('app-broken')
).render(<Type />)
ReactDOM.createRoot(
document.getElementById('app-working')
).render(<TypeWorking />)
<h1>Question (bug)</h1>
<div id="app-broken"></div>
<h1>Working</h1>
<div id="app-working"></div>
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
The question also mentions an animated cursor. One idea would be to use ::before/::after pseudo-elements (or another element like a <span>) with an infinite CSS animation.
I reworked Type function and it has worked as what you want. Let's try it.
function type() {
const timerId = setInterval(() => {
setChar((prev) => {
if (prev.length >= str.length) {
clearInterval(timerId);
return prev;
}
return prev + str[prev.length - 1 + 1];
});
}, 200);
}
You can refer at this link: https://codesandbox.io/s/musing-joji-rrpk6i?file=/src/App.js:155-451
Related
so, I have this function here
const [shopItems,setShopItems] = useState("");_
useEffect(()=> {
commerce.products.list().then((product) => {
setShopItems(product)
});
}, [])
function categorize(category) {
let categoryarray = []
if (shopItems!= "s") {
shopItems.data.forEach((el)=> {
for (let i =0; i < el.categories.length; i++) {
if (el.categories[i].slug == category) categoryarray.push(el)
}
})
}
return categoryarray;
}
the useEffect Hook was just for context, I'm mostly concerned about the categorize function. Is there anyway to optimize this since I have observed that my website scrolls very slowly, and I think this might be one of the culprits behind the slow scrolling speeds. Thanks in Advance!
The only way I can see to optimise that code is to exit as soon as a match is found. (I prefer using a while loop for this purpose).
shopItems.data.forEach(el => {
let idx = 0;
while (idx < el.categories.length) {
if (el.categories[idx].slug == category) {
categoryarray.push(el);
break;
} else {
idx++;
}
}
});
If you wanted something that looked slightly better (not mixing forEach and for, for example) you could use this: (no performance enhancements as far as I can see though)
shopItems.data.forEach(el => {
if (el.categories.map(({ slug }) => slug).includes(category)) categoryarray.push(el);
});
Or even use reduce:
let categoryarray = shopItems.data.reduce((a, c) => {
if (c.categories.map(({ slug }) => slug).includes(category) return a.concat(c);
else return a;
}, []);
The first option is still the most performant as realistically it will run through the loop less times.
You can use useMemo
https://reactjs.org/docs/hooks-reference.html#usememo
useMemo works by having 2 parameters. A function and an array of dependencies, if any of the dependencies change it re-runs the provided function and stores the value, next render if the dependencies havent changed it just uses the previous value.
const categories = useMemo(() =>
let categoryarray = []
if (shopItems!= "s") {
shopItems.data.forEach((el)=> {
for (let i =0; i < el.categories.length; i++) {
if (el.categories[i].slug == category) categoryarray.push(el)
}
})
}
return categoryarray;
}, [shopItems.data, category])
Is there a way where I can use for loops and if statements without breaking the hook rule? To elaborate, I am currently trying to compare two lists (allData and currentSelection) and if there are similarities, I will add them to another list (favData). However, I am constantly either having visibility issues or errors. If I can get some help, I would much appreciate it!
const [favData, setFavData] = useState([]);
useEffect(() => {
getFilterFavMeal();
}, []);
function getFilterFavMeal() {
allData.forEach((mealList) => {
currentSelection.forEach((mealList2) => {
if (mealList["menu_item"]["menu_item_id"] === mealList2.value) {
// with push, I have visibility issues
// favData.push(mealList);
setFavData(mealList);
}
});
});
setFavData(favData);
}
The set function that useState returns updates the state and schedules a re-render of the component so that the UI can update. It doesn't make sense to call the set function many times in one render.
You also don't want to mutate React state by using functions like push.
Since it looks like favData is deterministic, you can simply remove it from the component state and calculate it in the render loop.
const favData = allData.filter(a => currentSelection.some(c => c.value === a.menu_item.menu_item_id));
Answering your original question, of course you can use loops. As long as you don't mutate the existing state object. And don't set the state more than once per render.
const FF = () => {
const [list, setList] = useState([]);
const addStuffToList = () => {
const tail = Array.from(new Array(3)).map((_e, i) => i);
// Build a new array object and use that when setting state
setList([...list, ...tail]);
}
const forLoop = () => {
const tail = [];
for (let i = 0; i < 4; i++) {
tail.push(i);
}
// Same thing
setList([...list, ...tail]);
}
return ...
};
I have a working animation of an object made with the "useRef" hook. Part of the code in this animation will be repeated several times, so I moved it into a separate function, but when I try to call this function, when rendering the component, I get the error "Can't assign to property" scrollLeft "on 1: not an object" what could be the problem?
Full code on codesandbox
https://codesandbox.io/s/peaceful-silence-bm6hx?file=/src/scroll.js
import React, {useState, useEffect, useRef} from 'react'
const Scrollable = props => {
const items = props.items;
let ref = useRef()
const [state, setState] = useState({
isScrolling:false,
clientX:0,
scrollX:0
})
const [touchStart, setTouchStart] = useState(0);
let frameId;
const onMouseDown = e =>{...}
const onMouseUp = e =>{
if(ref && ref.current && !ref.current.contains(e.target)) {
return;
}
e.preventDefault()
let touchShift = touchStart - state.clientX
let rez;
let shift;
if(touchShift > 0) {
shift = 300 - touchShift
rez = state.scrollX + shift
if(rez>2100){
rez =1800
cancelAnimationFrame(frameId)
}
let speed = shift / 20
let cur = state.scrollX
frameId = requestAnimationFrame(animate)
animate(cur,speed,rez)
}
}
const animate = (cur, speed,rez) => {
frameId = requestAnimationFrame(animate)
cur = cur + speed
ref.current.scrollLeft = cur.toFixed(2)
if (Math.round(cur) === rez) {
cancelAnimationFrame(frameId)
setState({
...state,
scrollX:rez,
isScrolling:false,
})
}
}
useEffect(() =>{
document.addEventListener('mousedown',onMouseDown)
document.addEventListener('mouseup',onMouseUp)
return () => {
document.removeEventListener('mousedown',onMouseDown)
document.removeEventListener('mouseup',onMouseUp)
}
})
useEffect(() =>{
ref.current = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(ref.current)
},[]})
return (
<div className={classes.charPage}>
<div
ref={ref}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}>
</div>
</div>
)
}
export default Scrollable;
This error means you're trying to set a property on a number. In your useEffect you're doing this:
ref.current = requestAnimationFrame(animate)
requestAnimationFrame returns, according to MDN:
A long integer value, the request id, that uniquely identifies the entry in the callback list. This is a non-zero value, but you may not make any other assumptions about its value.
But you're also using the same ref for your DOM element. After your useEffect runs it will have set your ref to the rAF id which is a number causing your error when you try to set the scrollLeft property on the ref.
What you can try next to solve this is to use 2 separate refs, one for the requestAnimationFrame and one for your DOM element.
I'm new to learning React and Gatsby, and am trying to find the best way to apply simple a Javascript animation to a DOM element within a component. I know how to handle component events with onClick etc, but say for example I want to continuously change the colour of a <span> in my Header.js component every 2 seconds.
import React from 'react';
export default function Header() {
return (
<header>
<p>This is a <span>test!</span></p>
</header>
)
}
I'd then want to use some JS like:
const spanEl = document.querySelector('header span');
let counter = 0;
const changeColor = () => {
if (counter % 2 == 0) {
spanEl.style.color = "red";
} else {
spanEl.style.color = "blue";
}
counter++;
if (counter == 10) counter = 0;
}
setInterval(changeColor, 2000);
I found that I could put this inside a script tag in html.js before the closing body tag, but is there a way to keep this functionality within the component? Do I need to completely rethink my approach when working within this framework?
If you want to approach this with idiomatic React, then I would recommend expressing this behavior using hooks, component lifecycles, and effects.
The official React docs for hooks and effects are very good, I would start there.
import React from 'react';
const noop = () => null;
// Encapsulate the interval behavior
const useInterval = (callback, delay) => {
const savedCallback = useRef(noop);
useEffect(() => {
savedCallback.current = callback;
savedCallback.current();
}, [callback]);
useEffect(() => {
const id = setInterval(savedCallback.current, delay);
return () => clearInterval(id);
}, [delay]);
};
export default function Header() {
const [color, setColor] = useState("blue");
// setColor causes a re-render of the component
const updateColor = setColor(color === "blue" ? "red" : "blue");
useInterval(updateColor, 2000);
// Use the jsx to change the color instead of reaching into the dom
return (
<header>
<p>This is a <span style={{ color }}>test!</span></p>
</header>
)
}
[EDIT: I've just seen the answer from #windowsill, which I think is better than mine; I would recommend going with that solution.]
In a React functional component, you need to use the useReference hook to target an element (rather than selecting it with document.querySelector()) and the useEffecet hook to set and clear the timeout when the component mounts/unmounts:
import React, {
useEffect,
useRef,
useCallback
} from 'react';
export function Header() {
const animatedText = useRef(null);
const runAnimation = useCallback(elem => {
const currColour = elem.style.color;
elem.style.color = (currColour === 'red' && 'blue') || 'red';
}, []);
useEffect(() => {
const animationInterval = setInterval(() => {
runAnimation(animatedText.current);
}, 2000);
return () => {
clearInterval(animationInterval);
}
}, [runAnimation]);
return (
<header>
<p>This is a <span ref={animatedText}>test!</span></p>
</header>
);
}
The useCallback hook is used for optimization purposes and prevent the function runAnimation from being re-defined and initialized every time the component re-renders.
I'm building an autocomplete feature. Everything works as expected except when I reach the end of the input. When I change the value of the input, programatically, the input doesn't scroll/focus on the cursor until I type something.
I tried doing something like this on componentDidUpdate and it worked, but besides being a bit "dirty" I don't want to use the onBlur because I'm closing the autocomplete popup on this event.
setTimeout(() => {
this.refInput.input.selectionStart = this.refInput.input.selectionEnd = this.state.value.length;
this.refInput.input.blur();
this.refInput.input.focus();
}, 1);
How can I achieve this, specially without a setTimeout? If I do it without the setTimeout it doesn't work.
I'd do it with a useEffect Hook
import React, { useEffect } from 'react'
//
//
useEffect(()=>{
this.refInput.input.selectionStart = this.refInput.input.selectionEnd = this.state.value.length;
this.refInput.input.blur();
this.refInput.input.focus();
},[this.state.value, this.refInput.input.selectionStart, this.refInput.input.selectionEnd])
I made it worked. Unfortunately it is not very pretty. I still need to use the blur and setTimeout events.
onBlur = () => {
setTimeout(() => {
if (this.refInput.input !== document.activeElement) {
console.log(this.refInput.input.selectionEnd);
this.setState({ helperVisible: false });
this.props.clearAutoComplete();
}
}, 1);
};
(...)
componentDidUpdate(prevProps, prevState, snapshot) {
const { value } = this.state;
if (prevState.value !== value) {
setTimeout(() => {
this.refInput.input.selectionStart = this.refInput.input.selectionEnd = this.state.value.length;
this.refInput.input.blur();
this.refInput.input.focus();
}, 0);
}
If anyone has any ideia how to improve this it would be great. Otherwise I'm marking this as the correct answer.
The below should grab the element. focus on it. then move the selection range to the end of the current value. This abides by the desire to avoid blur().
CAVEAT: sometimes it doesn't seem to properly fire in this snippet and in codepen..
(function () {
const el = document.getElementById('dataList'),
val = 'this is my value',
len = val.length;
el.focus();
el.value = val;
setTimeout(function () { el.setSelectionRange(len, len); }, 100);
})();
<input id="dataList">