I'm trying to create a typing effect in React on 3 containers that are lined up and holding strings within them. The effect should start from the leftmost container after it finishes typing its string, it starts the container that comes after it, and so on.
I started with an idea where I store all the strings in an array and initialize a new array called currentText with a new letter every second but I probably just made things more complicated for myself.
Perhaps there is a simpler solution?
My complicated and not working solution looks like this:
const [text, setText] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [currentText, setCurrentText] = useState([]);
// Setting Text Content For Typing
useEffect(() => {
if(log && log.length > 0 && text.length == 0){
log.map((item, key) => {
let time = item['time'];
let message = item['message'];
let event = item['eventid'];
setText((prev) => [...prev, time, message, event]);
});
}
}, [log, text]);
useEffect(() => {
if(currentText?.length < text?.length){
const interval = setInterval(()=> {
// Setting Current index
if(currentText?.length !== 0 && currentIndex !== currentText?.length -1) {
setCurrentIndex(currentText?.length -1);
}
// Check if the last index string completed
if(currentText[currentIndex]?.length !== text[currentIndex]?.length){
let temp = currentText;
let lastText = temp[currentIndex];
if(lastText) lastText = lastText + text[currentIndex].charAt(lastText?.length -1);
else lastText = text[currentIndex].charAt(0);
temp[currentIndex] = lastText;
setCurrentText(temp);
}
// If completed open new array element to contain new string
else{
setCurrentText((prev) => [...prev, ['']]);
}
}, 1000);
return () => clearInterval(interval);
}
}, [currentText, text, currentIndex]);
return (
<>
{
currentText && currentText.length > 0 &&
currentText.map((item, key) => {
<div key={key} className={classes.row}>
<span className={classes.time}>{currentText[key]}</span>
<span className={classes.message}>{key % 1 ? currentText[key] : currentText[key+1]}</span>
<span className={classes.event}>{key % 2 ? currentText[key] : currentText[key+2]}</span>
</div>
})
}
The log look like this:
[
{time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string'},
{time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string'},
{time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string'},
]
// Define an array of log objects
const logs = [
{ time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string' },
{ time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string' },
{ time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string' },
];
// Define a component that renders a paragraph tag with a substring of text
function TextComponent(props) {
const { index = 0, text } = props;
return <p> {text.substring(0, index)}</p>;
}
// Define a component that types out the text of its children components
function TypeWriter(props) {
// Destructure the props and get the number of children
const { children, charTime = 50 } = props;
const nChildrens = React.Children.count(children);
// Throw an error if there are no children
if (nChildrens < 1) throw new Error('Type writer component must have children');
// Define the state of the component, which keeps track of which child is currently active and how much of its text has been typed out
const [activeComponent, setActiveComponent] = React.useState({
children: 0,
index: 0,
length: children[0].props.text.length,
});
// Use the useEffect hook to update the state every time the interval elapses
React.useEffect(() => {
// Set an interval that updates the state by typing out a single character every time it is called
const interval = setInterval(() => {
setActiveComponent((state) => {
// If the current child's text has been completely typed out, move on to the next child
if (state.index > state.length) {
if (state.children < nChildrens - 1)
return {
index: 0,
children: state.children + 1,
length: children[state.children + 1].props.text.length,
};
// If there are no more children, clear the interval and return the current state
clearInterval(interval);
return state;
}
// Otherwise, update the state to type out the next character
return { ...state, index: state.index + 1 };
});
}, charTime);
// Return a function to clear the interval when the component unmounts
return () => {
clearInterval(interval);
};
});
// Render the children components, with the active child being typed out and the others already fully typed out
return (
<div>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
key: index,
index:
index === activeComponent.children
? activeComponent.index
: index < activeComponent.children
? child.props.text.length
: 0,
});
})}
</div>
);
}
// Define a component that renders a TypeWriter component with TextComponents as its children
export default function Home() {
return (
<TypeWriter>
{logs.map((log, index) => (
<TextComponent key={index} text={log.message} />
))}
</TypeWriter>
);
}
Related
I am working on carousel slider. In which i use 2 arrays. 1 array 'slideData' of objects which contain images and id's and the other array 'slideNum' contain indexs to iterate. slideArr is the final array which we will map, it contain images from 'slideData' and map according to 'slideNum' indexes. When i update 'slideArr' array with useState than is not updating but when i update directly using array.splice than its working.
const SecondGrid = () => {
const len = SlideData.length - 1;
const [first, setFirst] = useState(1);
const [second, setSecond] = useState(2);
const [third, setThird] = useState(3);
let [slideNum, setSlideNum] = useState([0, 1, 2]);
const [slideArr, setSlideArr] = useState([
{
id: 0,
imgsrc: "./assets/c1.jpg",
data: "Here is Light salty chineese touch",
},
{
id: 1,
imgsrc: "./assets/c2.jpg",
data: "Try our special breakfast",
},
{
id: 2,
imgsrc: "./assets/c3.jpg",
data: "Taste the italian likalu food",
},
]);
const next = () => {
setFirst(first >= len ? 0 : (prevState) => prevState + 1);
setSecond(second >= len ? 0 : (prevState) => prevState + 1);
setThird(third >= len ? 0 : (prevState) => prevState + 1);
// const arr = [...slideArr];
// storing next index image into all three Cards
slideNum.forEach((val, key1) => {
SlideData.forEach((value, key) => {
if (key === val) {
slideArr.splice(key1, 1, value);
// this is not working
// arr[key1] = value;
// console.log(arr);
// setSlideArr(arr);
//console.log(slideArr);
}
});
});
};
useEffect(() => {
next();
}, []);
useEffect(() => {
const interval = setTimeout(() => {
//updaing slideNum number,in which 'first' contain id of image which will be on 0 index, its updating through useState.
setSlideNum([first, second, third]);
next();
}, 3000);
return () => clearTimeout(interval);
}, [first, second, third]);
return (
<Container>
<div className="row">
<div className="d-flex flex-wrap justify-content-around ">
{slideArr.map((val) => (
<SlideCard val={val} />
))}
</div>
</div>
</Container>
);
};
It would be helpful to have some standalone code that demonstrates the problem but just looking at the commented out bit you say is not working, you can use map() to change values in an array:
setSlideArr(slideArr.map(item => (item.id === 1 ? value : item)))
Here's a demo CodeSandbox.
Also, your console.log(slideArr) should be in a useEffect hook if you want to see the state value after it has changed.
As far as I know, what happens is that you can't splice slideArr because it's useState, what you would be better off doing is:
if (key === val) {
var arr = slideArr;
arr.splice(key1, 1, value);
setSlideArr(arr);
}
I've got the following search suggest with React hooks that uses react-hotkeys-hooks to manage keypress.
Why does selectedUserItem not update on keypress Enter? It stays 0 while the up and down keys change.
import { useHotkeys } from "react-hotkeys-hook";
import React, { useState } from "react";
import "./styles.css";
const itemsByName = [
{
id: 1,
name: "Ice Cream"
},
{
id: 2,
name: "Banana Pudding"
},
{
id: 3,
name: "Chocolate Cake"
},
{
id: 4,
name: "Sponge Cake"
},
{
id: 5,
name: "Carrot Cake"
}
];
const App = () => {
const [selectedUserItem, setSelectedUserItem] = useState(0);
// const [create] = useMutation(SAVE_USER_ITEM, {
// refetchQueries: ["UserItemsQuery"]
// })
const itemSelect = (e, item) => {
e.preventDefault();
// create({ variables: { input: { id: item.id } } });
// console.log(item)
};
const increment = selectedUserItem => {
const max = itemsByName.length - 1;
return max > selectedUserItem ? selectedUserItem + 1 : max;
};
const decrement = selectedUserItem => {
const min = 0;
return min < selectedUserItem ? selectedUserItem - 1 : min;
};
useHotkeys(
"*",
(event, handler) => {
// console.log(handler)
switch (event.key) {
case "ArrowDown":
setSelectedUserItem(selectedUserItem => increment(selectedUserItem));
break;
case "ArrowUp":
setSelectedUserItem(selectedUserItem => decrement(selectedUserItem));
break;
case "Enter":
console.log(selectedUserItem);
const userItem = itemsByName[selectedUserItem];
console.log(userItem);
break;
default:
console.log(event.key);
break;
}
},
{
filter: () => true
}
);
return (
<div className="absolute w-3/4 mt-16 ml-8 py-2 bg-white shadow-xl rounded-lg">
<h1>Index: {selectedUserItem}</h1>
{itemsByName.map((item, i) => {
return (
<div
href="#"
onClick={e => itemSelect(e, item)}
className={`${selectedUserItem === i ? "hovered" : ""} dessert`}
key={item.id}
>
{item.id}: {item.name}
</div>
);
})}
</div>
);
};
export default App;
useHotkeys internals use the useCallback and useEffect hooks, which need to know when some of its dependencies change. To make sure it works well with these hooks, useHotkeys offers to pass a deps array, like the other hooks mentioned, as its last parameter.
deps: any[] = []: The dependency array that gets appended to the memoization of the callback. Here you define the inner dependencies of your callback. If for example your callback actions depend on a referentially unstable value or a value that will change over time, you should add this value to your deps array. Since most of the time your callback won't depend on any unstable callbacks or changing values over time you can leave this value alone since it will be set to an empty array by default.
In your code, it would looks like this:
// These never changes and do not rely on the component scope, so they
// can be defined safely outside the component.
const increment = selectedUserItem => {
const max = itemsByName.length - 1;
return max > selectedUserItem ? selectedUserItem + 1 : max;
};
const decrement = selectedUserItem => {
const min = 0;
return min < selectedUserItem ? selectedUserItem - 1 : min;
};
const App = () => {
const [selectedUserItem, setSelectedUserItem] = useState(0);
useHotkeys(
"*",
(event, handler) => {
switch (event.key) {
case "ArrowDown":
setSelectedUserItem(increment);
break;
case "ArrowUp":
setSelectedUserItem(decrement);
break;
case "Enter":
console.log(selectedUserItem, itemsByName[selectedUserItem]);
break;
default:
console.log(event.key);
break;
}
},
{
filter: () => true
},
// The dependencies array which ensure that the data is up to date in the callback.
[selectedUserItem, setSelectedUserItem]
);
// rest of the component
I'm having some trouble with the React useState hook. I have a todolist with a checkbox button and I want to update the 'done' property to 'true' that has the same id as the id of the 'clicked' checkbox button. If I console.log my 'toggleDone' function it returns the right id. But I have no idea how I can update the right property.
The current state:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: false
},
]
})
const toggleDone = (id) => {
console.log(id);
}
return (
<div className="App">
<Todos todos={state.todos} toggleDone={toggleDone}/>
</div>
);
}
The updated state I want:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: true // if I checked this checkbox.
},
]
})
You can safely use javascript's array map functionality since that will not modify existing state, which react does not like, and it returns a new array. The process is to loop over the state's array and find the correct id. Update the done boolean. Then set state with the updated list.
const toggleDone = (id) => {
console.log(id);
// loop over the todos list and find the provided id.
let updatedList = state.todos.map(item =>
{
if (item.id == id){
return {...item, done: !item.done}; //gets everything that was already in item, and updates "done"
}
return item; // else return unmodified item
});
setState({todos: updatedList}); // set state to new object with updated list
}
Edit: updated the code to toggle item.done instead of setting it to true.
You need to use the spread operator like so:
const toggleDone = (id) => {
let newState = [...state];
newState[index].done = true;
setState(newState])
}
D. Smith's answer is great, but could be refactored to be made more declarative like so..
const toggleDone = (id) => {
console.log(id);
setState(state => {
// loop over the todos list and find the provided id.
return state.todos.map(item => {
//gets everything that was already in item, and updates "done"
//else returns unmodified item
return item.id === id ? {...item, done: !item.done} : item
})
}); // set state to new object with updated list
}
const toggleDone = (id) => {
console.log(id);
// copy old state
const newState = {...state, todos: [...state.todos]};
// change value
const matchingIndex = newState.todos.findIndex((item) => item.id == id);
if (matchingIndex !== -1) {
newState.todos[matchingIndex] = {
...newState.todos[matchingIndex],
done: !newState.todos[matchingIndex].done
}
}
// set new state
setState(newState);
}
Something similar to D. Smith's answer but a little more concise:
const toggleDone = (id) => {
setState(prevState => {
// Loop over your list
return prevState.map((item) => {
// Check for the item with the specified id and update it
return item.id === id ? {...item, done: !item.done} : item
})
})
}
All the great answers but I would do it like this
setState(prevState => {
...prevState,
todos: [...prevState.todos, newObj]
})
This will safely update the state safely. Also the data integrity will be kept. This will also solve the data consistency at the time of update.
if you want to do any condition do like this
setState(prevState => {
if(condition){
return {
...prevState,
todos: [...prevState.todos, newObj]
}
}else{
return prevState
}
})
I would create just the todos array using useState instead of another state, the key is creating a copy of the todos array, updating that, and setting it as the new array.
Here is a working example: https://codesandbox.io/s/competent-bogdan-kn22e?file=/src/App.js
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
title: "take out trash",
done: false
},
{
id: 2,
title: "wife to dinner",
done: false
},
{
id: 3,
title: "make react app",
done: false
}
]);
const toggleDone = (e, item) => {
const indexToUpdate = todos.findIndex((todo) => todo.id === item.id);
const updatedTodos = [...todos]; // creates a copy of the array
updatedTodos[indexToUpdate].done = !item.done;
setTodos(updatedTodos);
};
I am currently mapping through an array of 100 objects that look like the following {id: 1, name: 'User 1'}, {id: 2, name: 'User 2' ... all the way up to 100} and I am displaying them in jsx. I am then filtering through the results using an input field and checking to see if the formData from the input matches the items within my array and am returning possible matches of Users. The problem is when I search for ex: er 2 in my input; however, it will return every user with a 2 in their name like so: User 2, User 20, User 21, all the way up to 29. I would like my filter to be more strict. If I search: er 2, I would like only User 2 to come up. I am unsure of how to make my filtered results more explicit. My code is as follows.
here is the react module that generates the array of 100 users
export function getUsers(n) {
const users = []
for (let i = 1; i <= n; i++) {
users.push({ id: i, name: `User ${i}` })
}
return users
}
here is the component I am displaying my data and filtering through it using the input
import React, { useState } from 'react'
import { getUsers } from './Data'
const INITIAL_STATE = {
people: ''
}
const App = () => {
const [formData, setFormData] = useState(INITIAL_STATE)
const [person, setPerson] = useState(getUsers(100))
const handleInputChange = field => e => {
console.log(field + ' ' + e.currentTarget.value)
setFormData({ ...formData, [field]: e.currentTarget.value })
}
const deletePerson = peep => {
const deletePerson = person.filter(n => n !== peep)
setPerson(deletePerson)
}
return (
<div style={{ marginTop: 50 }}>
<input
id='people'
name='people'
onChange={handleInputChange('people')}
value={formData.people}
InputProps={{
disableUnderline: true
}}
/>
{person
.filter(person => {
if (formData.people === '') return true
return person.name
.toLowerCase()
.includes(formData.people.toLowerCase())
})
.map(peep => {
return (
<>
<div>{peep.name}</div>
<button onClick={() => deletePerson(peep)}>delete me</button>
</>
)
})}
</div>
)
}
export default App
Any and all suggestions would be greatly appreciated and further help me in my overall understanding of javascript and react.
You could use a regex like this:
{person
.filter(person => {
if (formData.people === '') return true
const escaped = escapeRegExp(formData.people);
const re = new RegExp(`.*${escaped}$`);
return re.test(person.name);
})
...
function escapeRegExp(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
However, I think that you may be making a mistake making the test stricter in this way. Most users won't expect a search to match the end of a string, which is what you seem to want, not an "exact" match per the question title.
Here is a different approach if you want to avoid using regex for performance reasons
Note that this approach works only if your input is unique er n
.filter(person => {
if (formData.people === '') return true
str = person.name
n = str.slice(2,str.length )
if(n==input) return person
})
However if your input is not unique you can use a regex pattern to detect the UserId in the input
const users = []
function getUsers(n) {
for (let i = 1; i <= n; i++) {
users.push({ id: i, name: `User ${i}` })
}
return users
}
getUsers(100)
input="use er100ers"
x=users.filter(person => {
if (input === '') return true
const str = person.name
const n = str.slice(5,str.length )
const inpid = input.match(/(\d+)/)
if(n==inpid[0]) return person
})
console.log(x)
I have a Chat component which uses API to populate the messages state, also there are different areas that have different chats which I pass as props to the component.
In this component I have 3 useEffects but I am interested in two of them which don't work properly. In the first useEffect I have some code that basically resets the messages state on area change to undefined. I need to do this to be able to distinguish between the API not being called yet where I display a loading component <Spinner /> or if the API has been called and it has retrieved an empty array to show the <NoData> component.
The problem that I have is that when I change areas the useEffects get triggered as they should but the first useEffect doesn't update the messages state to undefined before the second useEffect is called. And after a rerender because of history push the messages come as undefined but then the second useEffect doesn't get triggered anymore. I don't get why the state is not being updated in the first useEffect before the second. Also the weird thing is this used to work for me before now it doesn't. I changed some stuff up without pushing to git and now I am puzzeled. Code below:
export default function ChatPage({ history, match, area, ...props }) {
const [templates, setTemplates] = useState([]);
const [advisors, setAdvisors] = useState([]);
const [messages, setMessages] = useState(undefined);
const [conversation, setConversation] = useState([]);
const [chatToLoad, setChatToLoad] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [linkOrigin, setLinkOrigin] = useState("");
const [headerText, setHeaderText] = useState("");
// useEffect used to reset messages and conversation state
// triggered on area change(messages and conversation reset)
// and customer ID change(conversation reset).
// Required to distinguish between API call not being made yet
// and API returning no data.
useEffect(() => {
if (match.params.id) {
setLinkOrigin(match.params.id);
}
if (messages) {
if (match.params.id && messages.length !== 0) {
let matches = messages.filter(
(message) => message.ORIGINATOR === match.params.id
);
if (matches.length !== 0 && match.params.id === linkOrigin) {
setMessages(undefined);
history.push("/chats/" + match.params.area);
}
}
}
setConversation([]);
}, [area, match.params.id]);
// API calls
useEffect(() => {
if (templates.length === 0) {
api.getTemplates().then((templates) => {
setTemplates(templates);
});
}
if (advisors.length === 0) {
api.getAgents().then((advisors) => {
setAdvisors(advisors);
});
}
if (!messages || messages.length === 0) {
chooseQueue(match.params.area).then((queuesData) => {
let queues = queuesData.data.map((message) => ({
DATE_SORT: message.DATE_RECIEVED,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: "SMS_OUTBOUND",
ASSIGNED_TO: message.ASSIGNED_TO || null,
}));
setMessages(orderMessagesByDate(queues));
setChatToLoad(queues[0]);
});
}
}, [area]);
useEffect(() => {
if (messages) {
if (messages.length) {
let loadId = match.params.id ? match.params.id : messages[0].ORIGINATOR;
const params = {
MobileNumber: loadId,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: message.MSG_TYPE2.replace("MobileOriginated", "SMS"),
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
setChatToLoad(
messages.find((message) => message.ORIGINATOR === loadId)
);
history.push("/chats/" + match.params.area + "/" + loadId);
}
}
}, [messages]);
function chooseQueue(queueType) {
switch (queueType) {
case "myqueue":
setHeaderText("My chats");
return queuesApi.getMyActiveQueues(area);
case "mycompleted":
setHeaderText("My completed chats");
return queuesApi.getMyCompletedQueues();
case "queues":
setHeaderText("Chats");
return queuesApi.getQueues(area);
case "completed":
setHeaderText("Completed chats");
return queuesApi.getCompletedQueues();
default:
setHeaderText("My chats");
return queuesApi.getQueues(area);
}
}
function classifyMessage(message) {
return message.MSG_TYPE.includes("OUTBOUND") ||
message.MSG_TYPE.includes("FAULT_TEST")
? "outbound"
: "inbound";
}
async function submitMessage(message) {
var params = {
number: message.ORIGINATOR,
message: message.MESSAGE,
smssize: message.MESSAGE.length
};
await messagingApi.replyToCustomer(params).then((res) => {
if (res.data[0].RVALUE === "200") {
let extendedMsg = [...messages, message];
let extendedConversation = [...conversation, message];
setConversation([...extendedConversation]);
setMessages(orderMessagesByDate([...extendedMsg]));
}
});
}
function orderMessagesByDate(list) {
return list.sort(function(x, y) {
return new Date(y.DATE_SORT) - new Date(x.DATE_SORT);
});
}
const modalHandler = () => {
setIsOpen(!isOpen);
};
let chatConfig = {
channelSwitch: true,
channels: channels,
templateModal: true,
templates: templates,
advisorModal: true,
advisors: advisors,
};
const onActiveChatChange = (message) => {
history.push("/chats/" + match.params.area + "/" + message.ORIGINATOR);
const params = {
MobileNumber: message.ORIGINATOR,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
};
return (
<div data-test="component">
<BodyHeader
text={headerText}
children={
<FontAwesomeIcon
icon="plus-square"
aria-hidden="true"
size="2x"
onClick={modalHandler}
/>
}
/>
{messages && chatToLoad ? (
<>
<ChatWindow
messages={messages}
conversation={conversation}
chatToLoad={chatToLoad}
onActiveChatChange={onActiveChatChange}
classifyMessage={classifyMessage}
submitMessage={submitMessage}
config={chatConfig}
/>
<SendMessageModal isOpen={isOpen} toggle={modalHandler} />
</>
) : !messages ? (
<Spinner />
) : (
<NoDataHeader>There are no chats in this area</NoDataHeader>
)}
</div>
);
}
You can't get what you want this way. A state change applied in a useEffect won't have effect until the next rendering cycle, the following callbacks will still see the current const value.
If you want to change the value in the current rendering cycle the only option you have is to relax your const into let and set the variables yourself.
After all: you were expecting a const to change isn't it? ;)