useEffect hook misbehaves with setTimeout and state - javascript

I created a custom toast component in my exercise React application. It is working correctly until the moment I try to introduce an auto dismiss timeout functionality. Basically when you load a new toast it needs to dismiss itself after let say 5000ms.
If you want check the full code in my Github Repo that also have a live preview.
Easiest way to create toast is put invalid mail / password.
I believe I am doing something wrong with the useEffect hook or I am missing something. The problem is that when I am creating multiple toasts they disappear all at the same time. Also React is complaining that I didn't include remove as a dependency of the useEffect hook but when I do it becomes even worse. Can someone demystify why this is happening and how it can be fixed. I am a bit new to React.
Here is the file that creates a HOC around my main App component:
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import ToastContext from './context';
import Toast from './Toast';
import styles from './styles.module.css';
function generateUEID() {
let first = (Math.random() * 46656) | 0;
let second = (Math.random() * 46656) | 0;
first = ('000' + first.toString(36)).slice(-3);
second = ('000' + second.toString(36)).slice(-3);
return first + second;
}
function withToastProvider(Component) {
function WithToastProvider(props) {
const [toasts, setToasts] = useState([]);
const add = (content, type = 'success') => {
const id = generateUEID();
if (toasts.length > 4) {
toasts.shift();
}
setToasts([...toasts, { id, content, type }]);
};
const remove = id => {
setToasts(toasts.filter(t => t.id !== id));
};
return (
<ToastContext.Provider value={{ add, remove, toasts }}>
<Component {...props} />
{ createPortal(
<div className={styles.toastsContainer}>
{ toasts.map(t => (
<Toast key={t.id} remove={() => remove(t.id)} type={t.type}>
{t.content}
</Toast>
)) }
</div>,
document.body
) }
</ToastContext.Provider>
);
}
return WithToastProvider;
}
export default withToastProvider;
And the Toast component:
import React, { useEffect } from 'react';
import styles from './styles.module.css';
function Toast({ children, remove, type }) {
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => remove(), duration);
console.log(id);
return () => clearTimeout(id);
}, []);
return (
<div onClick={remove} className={styles[`${type}Toast`]}>
<div className={styles.text}>
<strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
{ children }
</div>
<div>
<button className={styles.closeButton}>x</button>
</div>
</div>
);
}
export default Toast;

Searching today for the solution I found it here
You will need to use useRef and its current property
Here is how I transformed the Toast component to work:
import React, { useEffect, useRef } from 'react';
import styles from './styles.module.css';
function Toast({ children, remove, type }) {
const animationProps = useSpring({opacity: .9, from: {opacity: 0}});
const removeRef = useRef(remove);
removeRef.current = remove;
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => removeRef.current(), duration);
return () => clearTimeout(id);
}, []);
return (
<div onClick={remove} className={styles[`${type}Toast`]}>
<div className={styles.text}>
<strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
{ children }
</div>
<div>
<button className={styles.closeButton}>x</button>
</div>
</div>
);
}
export default Toast;

Related

I want to get 3 random data (different from each other) using UseSelector/userReducer but I can't access the data

I want to get 3 random (but different from each other) user data using useSelector and display these data on the screen.but it always renders unnecessarily and I can't reach the result I wanted.Unfortunately I couldn't find where I made the mistake.
The final version of my code is as follows;
import * as React from "react";
import Avatar from "#mui/material/Avatar";
import Stack from "#mui/material/Stack";
import Box from "#mui/material/Box";
import Button from "#mui/material/Button";
import Typography from "#mui/material/Typography";
import { useSelector } from "react-redux";
import "./whoToFollow.css";
export default function WoToFollow() {
const { authReducer, userReducer } = useSelector((state) => state);
const authUser = authReducer?.user?.userId;
const userList = userReducer?.data;
var arrayMyUser = [];
console.log("arrayMyUser =>>", arrayMyUser);
var selected = [];
// console.log("selected =>>", selected);
function arrayUserList(callback) {
setTimeout(function () {
var arrayUsers = userList?.filter((user) => user?.userId !== authUser);
arrayUsers?.map((item) => arrayMyUser.push(item));
}, 1000);
callback();
}
function rand() {
setTimeout(function () {
for (var i = 0; i < 3; i++) {
var ran = arrayMyUser[Math.floor(Math.random() * arrayMyUser?.length)];
console.log("ran =>> ", ran);
if (selected.indexOf(ran) === -1) selected.push(ran);
}
}, 1000);
}
arrayUserList(rand);
return (
<Stack direction="row" spacing={2}>
{selected &&
selected?.map((p) => (
<Box className="whotofollowavatar">
<Avatar
className="whotoFollowAvatar"
alt={p?.name}
src={p?.avatar}
/>
<Typography variant="overline" display="block" gutterBottom>
{p?.name + p?.surname}
</Typography>
<Button className="whotoFollowButton" variant="contained">
Follow
</Button>
</Box>
))}
</Stack>
);
}
I tried to get three random data, but each time I got a different error. I got "undefined" for the first time, then I couldn't get the data randomly and Sometimes I didn't get any results because the data came late.. Finally, the data sometimes comes as I want, sometimes it doesn't come at all.
you have some problems and you are doing everything really complicated, some key points to have in mind using React:
every piece of code besides hooks inside a function component will run on every render, that means every variable you define will have a new value on every render (except for hooks)
if you need to store a value between renders, use a hook (useState or useRef are the most basic)
Here is your component using the useEffect + useState to store the suggestions list, (also you had a typo in the component name):
function WhoToFollow() {
const authUser = 3; // this is from your reducer
const userList = useRef([...dummyUserList]); // this is from your reducer, ignore the useRef, is for testing
const [suggestionList, setSuggestionList] = useState<any>([]); // replace any with the correct type
useEffect(() => {
if (authUser && userList) {
setSuggestionList(() => {
const { current: completeUserList } = userList;
const filteredList = completeUserList.filter(
(user) => user?.userId !== authUser
);
const newSuggestionList = [];
while (newSuggestionList.length < 3) {
const ran =
filteredList[Math.floor(Math.random() * filteredList?.length)];
if (newSuggestionList.indexOf(ran) === -1)
newSuggestionList.push(ran);
}
return newSuggestionList;
});
}
}, [userList, authUser]);
return (
<div>
userList:
{suggestionList.map((user: any) => (
<div>{user.name}</div>
))}
</div>
);
}
What I did:
store the generated list inside an useState
move all the logic to the useEffect and remove the functions, you don't need those
refactor the logic to generate 3 suggestions using a while, with the for it will not generate 3 suggestions every time, if you find a duplicated, it will not add a new one
Here is a codesandbox:
https://codesandbox.io/s/kind-silence-3e5s80?file=/src/App.tsx
You can try this, encapsulating the function within a useEffect hook. Also i feel like you it would be best to have states for arrayMyUser, and selected. That will be the "React" way of handling things.
import * as React from "react";
import Avatar from "#mui/material/Avatar";
import Stack from "#mui/material/Stack";
import Box from "#mui/material/Box";
import Button from "#mui/material/Button";
import Typography from "#mui/material/Typography";
import { useSelector } from "react-redux";
import "./whoToFollow.css";
export default function WoToFollow() {
const { authReducer, userReducer } = useSelector((state) => state);
const authUser = authReducer?.user?.userId;
const userList = userReducer?.data;
var arrayMyUser = [];
console.log("arrayMyUser =>>", arrayMyUser);
var selected = [];
// console.log("selected =>>", selected);
function arrayUserList(callback) {
setTimeout(function () {
var arrayUsers = userList?.filter((user) => user?.userId !== authUser);
arrayUsers?.map((item) => arrayMyUser.push(item));
}, 1000);
callback();
}
function rand() {
setTimeout(function () {
for (var i = 0; i < 3; i++) {
var ran = arrayMyUser[Math.floor(Math.random() * arrayMyUser?.length)];
console.log("ran =>> ", ran);
if (selected.indexOf(ran) === -1) selected.push(ran);
}
}, 1000);
}
useEffect(() => {
if (authUser && userList) {
arrayUserList(rand);
}
}, [authUser, userList]);
return (
<Stack direction="row" spacing={2}>
{selected &&
selected?.map((p) => (
<Box className="whotofollowavatar">
<Avatar
className="whotoFollowAvatar"
alt={p?.name}
src={p?.avatar}
/>
<Typography variant="overline" display="block" gutterBottom>
{p?.name + p?.surname}
</Typography>
<Button className="whotoFollowButton" variant="contained">
Follow
</Button>
</Box>
))}
</Stack>
);
}

How to get a link element from one list in the react component and run a link whit out clicking?

0
I have a list of messages and I am trying to get some link from that message if it exists there (and store it in studentlink const) this will happen in an event listener. getElemenBy tag and id and the name don't work, I don't know why! what is your idea? in the next step, I need to run that link whit out clicking, automatically but I didn't find any method or solution yet, how is possible?
import React from "react";
import { connect } from "react-redux";
import reactDom from "react-dom";
function MessageList(props) {
const handleLoad = (e) => {
if (listItems !== "") {
let studentlink = document.getElementById("myAnchor").href;
window.location.href = studentlink;
alert(`${studentlink}`);
} else {
console.log(window.location.href);
}
};
React.useEffect(() => {
window.addEventListener("load", handleLoad);
// cleanup this component
return () => {
window.removeEventListener("load", handleLoad);
};
}, []);
const arr = props.studentmessage;
const listItems = arr.map((message, index) => (
<li key={index} id="myAnchor">
{message}
</li>
));
return (
<section>
<div className="commentbox">
<ul>{listItems}</ul>
</div>
</section>
);
}
const mapStatetoprops = (state) => {
return {
studentmessage: state.message,
};
};
export default connect(mapStatetoprops)(MessageList);

How to pass id from one component to another component onclick of an element

I'm trying to pass this is as id as props to another component which is not a child of the component. I was considering using context but i wanted to know if there was another way to it, since I'm quite new to react I'm looking for a more efficient way.
This is the component where the id of the element clicked is being generated. When i logged it the data is correct an no problems was notified. I first tried passing it as props as seen below but since i didn't want it to be seen on that page i didn't pass it to the main return statement neither did i call the method in it, but then it returned undefined in the component where i wanted to make use of it
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom';
import Workspacelist from '../Workspace/Workspacelist';
function BoardList({ boards }) {
const [currentid, setcurrentid] = useState('')
const history = useHistory()
const navigate = (id) => {
setcurrentid(id);
console.log(id)
history.push(`/workspace/${id}`)
return(
<Workspacelist id = {id}/>
)
}
return (
<>
{
boards.map((board) => (
<li key={board.id} className="boardlist" style={styles} onClick={() => navigate(board.id)}>
<h3>{board.title}</h3>
</li>
))}
</>
)
}
export default BoardList
PS: Firebase is being incoporated in this project, i was thinking that might be the reason cause it's my first time using firebase so maybe I'm missing something since all the data is coming from the server
And this is the component i want to pass it to
import React, { useState, useEffect } from 'react'
import Firebase, { db } from '../Firebase/Firebase';
import { Todo } from './List';
function Workspacelist({ id }) {
const [updatedId] = useState(id)
const [show, setshow] = useState(false);
const [Todos, setTodos] = useState([]);//Todolist
const [ToDo, setToDo] = useState('');
useEffect(() => {
const docRef = db.collection("boards").doc(updatedId).get().then(doc => {
if (doc.exists) {
setTodos(doc.data().todo);
console.log("Document data:", doc.data().todo);
} else {
console.log("No such document!");
}
}).catch(function (error) {
console.log("Error getting document:", error);
});
return docRef
})
return (
<div className="workspacelist">
<div className="todo">
<div>
<b>To Do</b>
<b>...</b>
<Todo Todos={Todos} />
<span onClick={() => { setshow(current => !current) }} style={{ display: show ? 'none' : 'block' }}>+ Add a card</span>
</div>
<div className="add" style={{ display: show ? 'block' : 'none' }}>
<textarea placeholder="Enter a title for this card..." value={ToDo} onChange={(e) => { setToDo(e.target.value) }} />
<button className="addcard" onClick={one}>Add Card</button>
<button onClick={() => { setshow(current => !current) }}>X</button>
<button className="more">...</button>
</div>
</div>
</div>
)
}
export default Workspacelist
Thanks in advance i did appreciate the help even if i have to rewrite it just tell me the way you would do it if you were in my shoes
To navigate to another page, you just need history.push(/workspace/${id}).
You don't even need any state here.
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom';
import Workspacelist from '../Workspace/Workspacelist';
function BoardList({ boards }) {
const history = useHistory()
const navigate = (id) => {
history.push(`/workspace/${id}`)
}
return (
<>
{
boards.map((board) => (
<li key={board.id} className="boardlist" style={styles} onClick={() => navigate(board.id)}>
<h3>{board.title}</h3>
</li>
))}
</>
)
}
export default BoardList
To get the id param on the Workspace page, you will need to use the useRouteMatch hook from react-router-dom:
import { useRouteMatch } from 'react-router-dom';
function Workspacelist() {
const {
params: { id },
} = useRouteMatch('/workspace/:id');
console.log(id)
}
Let me know if it solves your problem.
If you use dom version 6, change the following parts that showed in #HichamELBSI answer.
useHistory should change into useNavigate.
useRouteMatch should change into useMatch.
After applying those, the codes should be
import { useNavigate} from 'react-router-dom';
const nav = useNavigate();
const navigate = (id) => {
nav(`/workspace/${id}`)
}
Then other part should be
import { useMatch } from 'react-router-dom';
function Workspacelist() {
const {
params: { id },
} = useMatch('/workspace/:id');
console.log(id)
}

How to remove an element from an array dynamically? React.js

There are two components, I want to implement an element array using the useContext hook, but when the button is clicked, the element is not removed, but on the contrary, there are more of them. Tell me what is wrong here. I would be very grateful!
First component:
import React from 'react';
import CartItem from './CartItem';
import Context from '../Context';
function Cart() {
let sum = 0;
let arrPrice = [];
let [products, setProducts] = React.useState([]);
let loacalProsucts = JSON.parse(localStorage.getItem('products'));
if(loacalProsucts === null) {
return(
<div className="EmptyCart">
<h1>Cart is empty</h1>
</div>
)
} else {
{loacalProsucts.map(item => products.push(item))}
{loacalProsucts.map(item => arrPrice.push(JSON.parse(item.total)))}
}
for(let i in arrPrice) {
sum += arrPrice[i];
}
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
return(
<Context.Provider value={{removeItem}}>
<div className="Cart">
<h1>Your purchases:</h1>
<CartItem products = {products} />
<h1>Total: {sum}$</h1>
</div>
</Context.Provider>
)
}
Second component:
import React, { useContext } from 'react';
import Context from '../Context';
function CartList({products}) {
const {removeItem} = useContext(Context);
return(
<div className="CartList">
<img src={products.image} />
<h2>{products.name}</h2>
<h3 className="CartInfo">{products.kg}kg.</h3>
<h2 className="CartInfo">{products.total}$</h2>
<button className="CartInfo" onClick={() => removeItem(products.id)}>×</button>
</div>
);
}
export default CartList;
Component with a context:
import React from 'react';
const Context = React.createContext();
export default Context;
Adding to the comment above ^^
It's almost always a mistake to have initialization expressions inside your render loop (ie, outside of hooks). You'll also want to avoid mutating your local state, that's why useState returns a setter.
Totally untested:
function Cart() {
let [sum, setSum] = React.useState();
const loacalProsucts = useMemo(() => JSON.parse(localStorage.getItem('products')));
// Init products with local products if they exist
let [products, setProducts] = React.useState(loacalProsucts || []);
useEffect(() => {
// This is actually derived state so the whole thing
// could be replaced with
// const sum = products.reduce((a, c) => a + c?.total, 0);
setSum(products.reduce((a, c) => a + c?.total, 0));
}, [products]);
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
...

Prevent Double tap in React native

How to prevent a user from tapping a button twice in React native?
i.e. A user must not be able tap twice quickly on a touchable highlight
https://snack.expo.io/#patwoz/withpreventdoubleclick
Use this HOC to extend the touchable components like TouchableHighlight, Button ...
import debounce from 'lodash.debounce'; // 4.0.8
const withPreventDoubleClick = (WrappedComponent) => {
class PreventDoubleClick extends React.PureComponent {
debouncedOnPress = () => {
this.props.onPress && this.props.onPress();
}
onPress = debounce(this.debouncedOnPress, 300, { leading: true, trailing: false });
render() {
return <WrappedComponent {...this.props} onPress={this.onPress} />;
}
}
PreventDoubleClick.displayName = `withPreventDoubleClick(${WrappedComponent.displayName ||WrappedComponent.name})`
return PreventDoubleClick;
}
Usage
import { Button } from 'react-native';
import withPreventDoubleClick from './withPreventDoubleClick';
const ButtonEx = withPreventDoubleClick(Button);
<ButtonEx onPress={this.onButtonClick} title="Click here" />
Use property Button.disabled
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, View, Button } from 'react-native';
export default class App extends Component {
state={
disabled:false,
}
pressButton() {
this.setState({
disabled: true,
});
// enable after 5 second
setTimeout(()=>{
this.setState({
disabled: false,
});
}, 5000)
}
render() {
return (
<Button
onPress={() => this.pressButton()}
title="Learn More"
color="#841584"
disabled={this.state.disabled}
accessibilityLabel="Learn more about this purple button"
/>
);
}
}
// skip this line if using Create React Native App
AppRegistry.registerComponent('AwesomeProject', () => App);
Here is my simple hook.
import { useRef } from 'react';
const BOUNCE_RATE = 2000;
export const useDebounce = () => {
const busy = useRef(false);
const debounce = async (callback: Function) => {
setTimeout(() => {
busy.current = false;
}, BOUNCE_RATE);
if (!busy.current) {
busy.current = true;
callback();
}
};
return { debounce };
};
This can be used anywhere you like. Even if it's not for buttons.
const { debounce } = useDebounce();
<Button onPress={() => debounce(onPressReload)}>
Tap Me again and adain!
</Button>
Agree with Accepted answer but very simple way , we can use following way
import debounce from 'lodash/debounce';
componentDidMount() {
this.onPressMethod= debounce(this.onPressMethod.bind(this), 500);
}
onPressMethod=()=> {
//what you actually want on button press
}
render() {
return (
<Button
onPress={() => this.onPressMethod()}
title="Your Button Name"
/>
);
}
I use it by refer the answer above. 'disabled' doesn't have to be a state.
import React, { Component } from 'react';
import { TouchableHighlight } from 'react-native';
class PreventDoubleTap extends Component {
disabled = false;
onPress = (...args) => {
if(this.disabled) return;
this.disabled = true;
setTimeout(()=>{
this.disabled = false;
}, 500);
this.props.onPress && this.props.onPress(...args);
}
}
export class ButtonHighLight extends PreventDoubleTap {
render() {
return (
<TouchableHighlight
{...this.props}
onPress={this.onPress}
underlayColor="#f7f7f7"
/>
);
}
}
It can be other touchable component like TouchableOpacity.
If you are using react navigation then use this format to navigate to another page.
this.props.navigation.navigate({key:"any",routeName:"YourRoute",params:{param1:value,param2:value}})
The StackNavigator would prevent routes having same keys to be pushed in the stack again.
You could write anything unique as the key and the params prop is optional if you want to pass parameters to another screen.
The accepted solution works great, but it makes it mandatory to wrap your whole component and to import lodash to achieve the desired behavior.
I wrote a custom React hook that makes it possible to only wrap your callback:
useTimeBlockedCallback.js
import { useRef } from 'react'
export default (callback, timeBlocked = 1000) => {
const isBlockedRef = useRef(false)
const unblockTimeout = useRef(false)
return (...callbackArgs) => {
if (!isBlockedRef.current) {
callback(...callbackArgs)
}
clearTimeout(unblockTimeout.current)
unblockTimeout.current = setTimeout(() => isBlockedRef.current = false, timeBlocked)
isBlockedRef.current = true
}
}
Usage:
yourComponent.js
import React from 'react'
import { View, Text } from 'react-native'
import useTimeBlockedCallback from '../hooks/useTimeBlockedCallback'
export default () => {
const callbackWithNoArgs = useTimeBlockedCallback(() => {
console.log('Do stuff here, like opening a new scene for instance.')
})
const callbackWithArgs = useTimeBlockedCallback((text) => {
console.log(text + ' will be logged once every 1000ms tops')
})
return (
<View>
<Text onPress={callbackWithNoArgs}>Touch me without double tap</Text>
<Text onPress={() => callbackWithArgs('Hello world')}>Log hello world</Text>
</View>
)
}
The callback is blocked for 1000ms after being called by default, but you can change that with the hook's second parameter.
I have a very simple solution using runAfterInteractions:
_GoCategoria(_categoria,_tipo){
if (loading === false){
loading = true;
this.props.navigation.navigate("Categoria", {categoria: _categoria, tipo: _tipo});
}
InteractionManager.runAfterInteractions(() => {
loading = false;
});
};
Did not use disable feature, setTimeout, or installed extra stuff.
This way code is executed without delays. I did not avoid double taps but I assured code to run just once.
I used the returned object from TouchableOpacity described in the docs https://reactnative.dev/docs/pressevent and a state variable to manage timestamps. lastTime is a state variable initialized at 0.
const [lastTime, setLastTime] = useState(0);
...
<TouchableOpacity onPress={async (obj) =>{
try{
console.log('Last time: ', obj.nativeEvent.timestamp);
if ((obj.nativeEvent.timestamp-lastTime)>1500){
console.log('First time: ',obj.nativeEvent.timestamp);
setLastTime(obj.nativeEvent.timestamp);
//your code
SplashScreen.show();
await dispatch(getDetails(item.device));
await dispatch(getTravels(item.device));
navigation.navigate("Tab");
//end of code
}
else{
return;
}
}catch(e){
console.log(e);
}
}}>
I am using an async function to handle dispatches that are actually fetching data, in the end I'm basically navigating to other screen.
Im printing out first and last time between touches. I choose there to exist at least 1500 ms of difference between them, and avoid any parasite double tap.
You can also show a loading gif whilst you await some async operation. Just make sure to tag your onPress with async () => {} so it can be await'd.
import React from 'react';
import {View, Button, ActivityIndicator} from 'react-native';
class Btn extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false
}
}
async setIsLoading(isLoading) {
const p = new Promise((resolve) => {
this.setState({isLoading}, resolve);
});
return p;
}
render() {
const {onPress, ...p} = this.props;
if (this.state.isLoading) {
return <View style={{marginTop: 2, marginBottom: 2}}>
<ActivityIndicator
size="large"
/>
</View>;
}
return <Button
{...p}
onPress={async () => {
await this.setIsLoading(true);
await onPress();
await this.setIsLoading(false);
}}
/>
}
}
export default Btn;
My implementation of wrapper component.
import React, { useState, useEffect } from 'react';
import { TouchableHighlight } from 'react-native';
export default ButtonOneTap = ({ onPress, disabled, children, ...props }) => {
const [isDisabled, toggleDisable] = useState(disabled);
const [timerId, setTimerId] = useState(null);
useEffect(() => {
toggleDisable(disabled);
},[disabled]);
useEffect(() => {
return () => {
toggleDisable(disabled);
clearTimeout(timerId);
}
})
const handleOnPress = () => {
toggleDisable(true);
onPress();
setTimerId(setTimeout(() => {
toggleDisable(false)
}, 1000))
}
return (
<TouchableHighlight onPress={handleOnPress} {...props} disabled={isDisabled} >
{children}
</TouchableHighlight>
)
}

Categories

Resources