I am building an animation where the letters of two words appear one by one, similar to a slide-in effect. I have the code made with jQuery, but I need to implement it in my React app (built with hooks). The code that I have takes the text, splits it creating individual letters, and adds spans between those letters. This is the following code that I need to convert to React:
const logoText = document.querySelector('.logo');
const stringText = logoText.textContent;
const splitText = stringText.split("");
for (let i=0; i < splitText.length; i++) {
text.innerHTML += "<span>" + splitText + "</span>"
}
let char = 0;
let timer = setInterval(onTick, 50)
I was wondering if you guys could help me figure it out. Thanks a lot!
You need to iterate over the text and create a timeout function for every letter with a different time of execution, that way will be visible the slide effect you are expecting:
Custom hook
const useSlideInText = text => {
const [slide, setSlide] = useState([]);
useEffect(() => {
Array.from(text).forEach((char, index) => {
const timeout = setTimeout(
() =>
setSlide(prev => (
<>
{prev}
<span>{char}</span>
</>
)),
index * 100
);
});
}, []);
return slide;
};
Usage
function App() {
const slide = useSlideInText("hello");
return (
<div>
{slide}
</div>
);
}
Working example
I am assuming the React components that you want to run this hook in possess the text you want to split. I am also assuming that on the interval, you want to reveal more of the text. In that case my example solution would look like this:
Hook
import {useState, useEffect} from "react";
const useSlideInText = (text) => {
const [revealed, setRevealed] = useState(0);
useEffect(() => {
if (revealed < text.length) {
setTimeout(() => setRevealed(revealed + 1), 50);
}
});
return text.split('').slice(0, revealed).map((char) => (<span>{char}</span>));
}
Example usage
const MyComponent = (props) => {
const displayText = useSlideInText(props.text);
return <div>{displayText}</div>;
};
going off of the other answer:
const generateDisplayTest = (text, numChars) => text.split('').slice(0, numChars).map((char) => (<span>{char}</span>));
const MyComponent = (props) => {
const [revealed, setRevealed] = useState(0);
useEffect(() => {
if (revealed < props.text.length) {
setTimeout(() => setRevealed(revealed + 1), 50);
}
}, [revealed]);
const displayText = generateDisplayTest(props.text, revealed);
return <div>{displayText}</div>;
};
including [revealed] in the useEffect means that useEffect will run every time that revealed changes. Also I always feel that useState/useEffect should live on the component, it has been that way in the place I worked but I'm not sure if that is industry standard.
Related
I am building a React functional component that uses some state variables, and I am trying to modify some of these variables from an external function thats called on a button click event, but when I pass the reference to the state methods to this external function, all of them are undefined. What could be the cause? If I just put the exact same code within the functional component, it works perfectly as intended.
import React from "react";
import {CodeArea, Table, EmptyField, Button} from '../util/util.js'
import {Step, Load} from "./buttons.js" // The external function in question, Loadfunction
Core(props){
const registersInitial = new Array(32).fill(0);
let buttonNamesInitial = ['LOAD','play', 'step-forward', 'run-to', 'step-back','pause','halt', 'rerun', 'add'];
const [bigText, setText] = React.useState();
const [registers, setRegisters] = React.useState(registersInitial);
const [running, setRunning] = React.useState(false);
const [programCounter, setProgramCounter] = React.useState(0);
const [buttonNames, setButtonNames] = React.useState(buttonNamesInitial);
const [lines, setLines] = React.useState([]);
const getBigText = () => {
return bigText;
}
const getRunning = () =>{
return running;
}
const getButtonNames = () => {
return buttonNames;
}
//... some code here thats irrelevant
function getQuickbarContents(){
let functions = [ //Backend will come here
() => Load(setRunning, getRunning, setButtonNames, getButtonNames, setProgramCounter, setLines, getBigText), //Where Load gets called
() => console.log("code running..."),
() => console.log("stepping to next line..."),
() => console.log("running to location..."),
() => console.log("stepping..."),
() => console.log("pausing..."),
() => console.log("halting..."),
() => console.log("running again..."),
() => console.log("select widget to add...")
]
let contents = [];
let row = [];
for (let i = 0; i<9; i++){
row.push(<Button onClick ={functions[i]} className='quickbar' name={buttonNames[i]}/>);
contents.push(row);
row = [];
}
return contents
}
const divs = [];
let buttons = getQuickbarContents();
divs.push(<div key='left' className='left'><Table name='quickbar' rows='7' cols='1' fill={buttons}/> </div>);
//... some more code to push more components do divs
return divs;}
export default Core;`
Button looks like this:
function Button({onClick, className, name}){
return <button onClick={onClick} className={className} name={name}>{name}</button>
}
and Load like this:
export function Load({setRunning, getRunning, setButtonNames, getButtonNames, setProgramCounter, setLines, getBigText}){
let newButtonName;
if (!getRunning()){ // Functions errors out with getRunning() undefined
herenewButtonName = "Reload";
}
else{ //while running if user wants to reload
newButtonName = "LOAD";
}
let lines = getBigText().split(/\n/);
setLines(lines);
setProgramCounter(0);
setRunning(!getRunning());
const newButtonNames = getButtonNames().map((value, index) =>{
if (index === 0){
return (newButtonName);
}
return value;
})
setButtonNames(newButtonNames);
}
So essentially in my head the flow should be: state methods initialised -> button components created -> wait for click of a button -> update state variablesBut clearly, something goes wrong along the way.
I've tried using inspection mode debugging, which revealed that in fact all of the parameters to Load are undefined when they are evaluated.
Note, that everything works as intended if I change the code up like this, eg. just put the whole function within the React component;
//... everything same as before
function getQuickbarContents(){
let functions = [
() =>{
let newButtonName;
if (!getRunning()){ //User clicks to start running
newButtonName = "Reload";
}
else{
newButtonName = "LOAD";
}
let lines = getBigText().split(/\n/);
setLines(lines);
setProgramCounter(0);
setRunning(!getRunning());
const newButtonNames = getButtonNames().map((value, index) =>{
if (index === 0){
return (newButtonName);
}
return value;
})
setButtonNames(newButtonNames)},
() => console.log("code running..."),
() => console.log("stepping to next line..."),
() => console.log("running to location..."),
() => Step(setRegisters, registers, setProgramCounter, programCounter, lines[programCounter]),
() => console.log("pausing..."),
() => console.log("halting..."),
() => console.log("running again..."),
() => console.log("select widget to add...")
]
//...everything same as before
so consequently the error is somewhere in the way I pass parameters to Load, or maybe I'm doing something I shouldn't be doing in React. Either way I have no clue, any ideas?
Problem was in the way parameters were defined in Load, as #robin_zigmond pointed out. Fixed now.
so I am trying to build an amazon clone to learn nextjs. I am trying to use react-context to save the chosen product's id number to an array. the data is being saved and I can access it from any of the components in the project but whenever a product with an id that has 2 digits the array.length increases by 2. here is my code
`
import React, { createContext, useContext, useState } from 'react';
const AppContext = createContext();
export function AppWrapper({ children }) {
var [basket, addToBasket]= useState([]);
return (
<AppContext.Provider value={[basket, addToBasket]}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
function Product({id, title, price, description, category, image }) {
var [basket, addToBasket] = useAppContext();
const addItemToBasket = () => {
addToBasket(basket + id);
}
return(
<button onClick={addItemToBasket} className='button'>Add to Basket</button>
<h1>items ID in basket: {basket}</h1>
<h1>length of array: {basket.length}</h1>
)
I did try this and I couldn't get it to work:
let counter = 0;
const addItemToBasket = () => {
for (let i = 0; i < basket.length; i++) {
if (basket[i].status === '0') counter++;
};
addToBasket(basket + id);
}
...
<h1>length of array: {counter}</h1>
I am pretty new to javascript so I did a lot of different variations of this. I gave the for loop its own function, I kept it out on its own but nothing I tried could get it to work. thanks for the help y'all.
useState returns a getter and a setter, not a getter and a pusher. Your addToBasket sets the value of the basket property to (basket + id), which will be a string. More specifically, basket is originally an array, but + on any arguments that are non-numeric results in a string; once basket is a string, basket + id will grow the string by the number of characters in the string representation of id.
To use the setter correctly:
const [basket, setBasket] = useState([]);
const addItemToBasket = () => {
setBasket([...basket, basket + id]);
}
this is the fetchData function and it works the first time, the table updated 10 rows, but when you reach the end again nothing happens . what is the reason for this to work the first time only and not updating after ?
const [items , setItems] = useState(data);
const [hasMore , setHasMore] = useState(true)
const [offset, setOffset] = useState(10);
const fetchMoreData = () => {
if (items.length >= items.length +1) {
setHasMore( false );
return;
}
setTimeout(() => {
setOffset( offset + 10);
}, 100);
};
You would need to provide a bit more code than this, but I suppose what you'll want to do is listen for changes in useEffect, and based on a condition, call your fetch function
I'm trying to build a component where an array of values are presented like a slider, where every time, the old content gets replaced by a new one, and that can be done using the buttons next and previous. The component is working, but I'm struggling a little bit in the edge cases, where I have to disable the buttons
I'll leave the link to a codesandbox where the component is being built, I'm sure it'll be easier to understand what's going on.
Link to sandbox
Try not to use state value for tracking button disabled status. Please check below.
import React, { useState } from "react";
export default function App() {
const data = ["q", "c", "s", "a"];
const [iterator, setIterator] = useState(0);
const [curr, setCurr] = useState(data[iterator]);
const fetchNext = () => {
if (iterator === data.length - 1) {
return;
}
setIterator((prev) => prev + 1);
setCurr(data[iterator + 1]);
};
const fetchPrevious = () => {
if (iterator === 0) {
return;
}
setIterator((prev) => prev - 1);
setCurr(data[iterator - 1]);
};
const nextDisabled = iterator >= data.length - 1;
const prevDisabled = iterator <= 0;
return (
<div>
<h1>{curr}</h1>
<button disabled={nextDisabled} onClick={fetchNext}>
next
</button>
<button disabled={prevDisabled} onClick={fetchPrevious}>
previous
</button>
</div>
);
}
The program should take the input user typed, search the data and return results in a drop down list.
When the userinput is more than 3 symbols, the Search() is called and I get "Error: Too many re-renders". Can't find where is the render loop.
import LTCityNames from "../lt-city-names.json"; //JSON object
const Openweathermap = () => {
const [searchList, setSearcList] = useState([]); //drop down list according to search word
const [text, setText] = useState(""); //text in the input field
const Search = (userinput) => {
let correctResult = "";
let dropdownList = [];
const regex = new RegExp(`^${userinput}`, "i");
for (let i = 0; i < LTCityNames.length; i++) {
correctResult = regex.test(LTCityNames[i].name);
if (correctResult){
dropdownList.push(LTCityNames[i]);
setSearcList(dropdownList);
}
}
};
const onChangeInput = (userinput) => {
setText(userinput);
if (userinput.length > 2) {
Search(userinput);
}
};
return (
<input
value={text}
onChange={(e) => {onChangeInput(e.target.value)} }
type="text"
placeholder="Enter address"
></input>
<div id="myDropdownWeather" className="dropdown-content">
{searchList.map((itemInArray) => {
return (
<ul>
<li>{itemInArray.name}</li>
</ul>
);
})
}
I think you must use useEffect like this:
const [text, setText] = useState(""); //text in the input field
const lastFilter = useRef(text);
useEffect(() => {
if (lastFilter.current !== text && text.lenght>2) {
Search(userinput);
lastFilter.current = text;
}
}, [text]);
const onChangeInput = (event) => {
var userinput=event.target.value;
setText(userinput);
};
and change
onChange={(e) => {onChangeInput(e.target.value)} }
to
onChange={(e) => {onChangeInput(e)} }
First: Why you are getting "Error: Too many re-renders"?
When you are using React Functional Components, every time you call a "setState" React reload all your Component, and since you are using functions inside you component these functions are also being loaded every single time your component change. So, when you type your search, the element will re-render uncontrollably.
Solving the problem:
Every time you want to use a function inside a React Functional Component you must use React.useCallback because this way you can control exactly when a function should be reloaded in memory preventing the errors you are getting.
One more thing, inside your return when you are working with react you cannot return more than one JSX Element, this will also cause you a lot of problems, to solve this you can use the fragment element <> ... </> or any other master element that will hold all the others (fragment elements will not interfere with you CSS).
The Code:
import React, { useCallback, useState } from 'react';
import LTCityNames from '../lt-city-names.json'; // JSON object
const Openweathermap = () => {
const [searchList, setSearcList] = useState([]); // drop down list according to search word
const [text, setText] = useState(''); // text in the input field
const Search = useCallback((userinput) => {
const correctResult = '';
const dropdownList = [];
const regex = new RegExp(`^${userinput}`, 'i');
for (let i = 0; i < LTCityNames.length; i++) {
const correctResult = regex.test(LTCityNames[i].name);
if (correctResult) {
dropdownList.push(LTCityNames[i]);
setSearcList(dropdownList);
}
}
}, []);
const onChangeInput = useCallback(
(e) => {
const userinput = e.target.value;
setText(userinput);
if (userinput.length > 2) {
Search(userinput);
}
},
[Search],
);
return (
<> // Fragment element start
<input
value={text}
onChange={(e) => onChangeInput(e)}
type="text"
placeholder="Enter address"
/>
<div id="myDropdownWeather" className="dropdown-content">
{searchList.map((itemInArray) => {
return (
<ul>
<li>{itemInArray.name}</li>
</ul>
);
})}
</div>
</> // Fragment element end
);
};
Understanding useCallback:
useCallback is a React function that will receive 2 parameters the first one is your function and the second one is an array of parameters that when changed will trigger a reload in memory for the function (every time you use an element that came from outside the function itself you need to use it as a parameter to reload the function in memory).
const myReactFunction = useCallback(() => {}, [a,b,c....] )
Improving you Component Return:
You are not required to use any of the tips listed bellow but they will improve the readability of your code.
Since you are calling your input onChange with (e) => onChangeInput(e) you can change your input to only onChangeInput:
<input
value={text}
onChange={onChangeInput} // same as (e) => function(e)
type="text"
placeholder="Enter address"
/>
The second tip is inside you map function, since you are using arrow functions you are not required to type return():
{searchList.map((itemInArray) => (
<ul>
<li>{itemInArray.name}</li>
</ul>
))}
import LTCityNames from "../lt-city-names.json"; //JSON object
const Openweathermap = () => {
const [searchList, setSearcList] = useState([]); //drop down list according to search word
const [text, setText] = useState(""); //text in the input field
const Search = (userinput) => {
let correctResult = "";
let dropdownList = [];
const regex = new RegExp(`^${userinput}`, "i");
for (let i = 0; i < LTCityNames.length; i++) {
correctResult = regex.test(LTCityNames[i].name);
if (correctResult){
dropdownList.push(LTCityNames[i]);
setSearcList(dropdownList);
}
}
};
const onChangeInput = (userinput) => {
setText(userinput);
if (userinput.length > 2) {
Search(userinput);
}
};
//remove value={text}
return (
<input
onChange={(e) => {onChangeInput(e.target.value)} }
type="text"
placeholder="Enter address"
></input>
<div id="myDropdownWeather" className="dropdown-content">
{searchList.map((itemInArray) => {
return (
<ul>
<li>{itemInArray.name}</li>
</ul>
);
})
}
Remove value = {text}