I am trying to make kind of flash message displayer to display success, error, warning messages at the top for a certain duration.
I have made the use of useRef hook to store timeouts so that I can clear it incase component unmounts before timeout completion.
Everything works as expected except, if the component unmounts before timeout callback, it does not clear the timeout which indeed is trying to setState which gives
Warning: Can't perform a React state update on an unmounted component
import React, { useEffect, useRef, useState } from 'react'
import SuccessGreen from '../../assets/SuccessGreen.svg'
import Cross from '../../assets/Cancel.svg'
import WarningExclamation from '../../assets/WarningExclamation.svg'
const ICONS_MAP = {
"warning": WarningExclamation,
"success": SuccessGreen,
"error": ""
}
export const FlashMessages = ({
duration=5000,
closeCallback,
pauseOnHover=false,
messageTheme='warning',
typoGraphy={className: 'text_body'},
firstIcon=true,
...props
}) => {
const [isDisplayable, setIsDisplayable] = useState(true)
const resumedAt = useRef(null)
const remainingDuration = useRef(duration)
const countDownTimer = useRef(null)
useEffect(() => {
countDownTimer.current = resumeDuration()
console.log(countDownTimer, "From mount")
return () => {clearTimeout(countDownTimer.current)}
}, [])
const resumeDuration = () => {
clearTimeout(countDownTimer.current)
resumedAt.current = new Date()
return setTimeout(() => forceCancel(), remainingDuration.current)
}
const pauseDuration = () => {
if(pauseOnHover){
clearTimeout(countDownTimer.current)
remainingDuration.current = remainingDuration.current - (new Date() - resumedAt.current)
}
}
const forceCancel = () => {
console.log(countDownTimer, "From force")
clearTimeout(countDownTimer.current);
setIsDisplayable(false);
closeCallback(null);
}
return isDisplayable ? (
<div onMouseEnter={pauseDuration} onMouseLeave={resumeDuration}
className={`flash_message_container ${messageTheme} ${typoGraphy.className}`} style={props.style}>
{ firstIcon ? (<img src={ICONS_MAP[messageTheme]} style={{marginRight: 8, width: 20}} />) : null }
<div style={{marginRight: 8}}>{props.children}</div>
<img src={Cross} onClick={forceCancel} style={{cursor: 'pointer', width: 20}}/>
</div>
):null
}
I have tried to mimic the core functionality of this npm package
https://github.com/danielsneijers/react-flash-message/blob/master/src/index.jsx
but whith functional component.
I think the problem is that when the mouseleave event happens, the timeout id returned by resumeDuration is not saved in countDownTimer.current, so the timeout isn't cleared in the cleanup function returned by useEffect.
You could modify resumeDuration to save the timeout id to countDownTimer.current instead of returning it:
countDownTimer.current = setTimeout(() => forceCancel(), remainingDuration.current)
and then, inside useEffect, just call resumeDuration, so the component would look like this:
import React, { useEffect, useRef, useState } from 'react'
import SuccessGreen from '../../assets/SuccessGreen.svg'
import Cross from '../../assets/Cancel.svg'
import WarningExclamation from '../../assets/WarningExclamation.svg'
const ICONS_MAP = {
"warning": WarningExclamation,
"success": SuccessGreen,
"error": ""
}
export const FlashMessages = ({
duration=5000,
closeCallback,
pauseOnHover=false,
messageTheme='warning',
typoGraphy={className: 'text_body'},
firstIcon=true,
...props
}) => {
const [isDisplayable, setIsDisplayable] = useState(true)
const resumedAt = useRef(null)
const remainingDuration = useRef(duration)
const countDownTimer = useRef(null)
useEffect(() => {
resumeDuration()
console.log(countDownTimer, "From mount")
return () => {clearTimeout(countDownTimer.current)}
}, [])
const resumeDuration = () => {
clearTimeout(countDownTimer.current)
resumedAt.current = new Date()
countDownTimer.current = setTimeout(() => forceCancel(), remainingDuration.current)
}
const pauseDuration = () => {
if(pauseOnHover){
clearTimeout(countDownTimer.current)
remainingDuration.current = remainingDuration.current - (new Date() - resumedAt.current)
}
}
const forceCancel = () => {
console.log(countDownTimer, "From force")
clearTimeout(countDownTimer.current);
setIsDisplayable(false);
closeCallback(null);
}
return isDisplayable ? (
<div onMouseEnter={pauseDuration} onMouseLeave={resumeDuration}
className={`flash_message_container ${messageTheme} ${typoGraphy.className}`} style={props.style}>
{ firstIcon ? (<img src={ICONS_MAP[messageTheme]} style={{marginRight: 8, width: 20}} />) : null }
<div style={{marginRight: 8}}>{props.children}</div>
<img src={Cross} onClick={forceCancel} style={{cursor: 'pointer', width: 20}}/>
</div>
):null
}
and it would then mimic the logic from https://github.com/danielsneijers/react-flash-message/blob/master/src/index.jsx
Related
I'm learning ReactTs and I'm coding a simple two components app to Upload a file and with another request obtain upload progress, but I can't receive the progress data from request. I think useEffect never execute the axios get request. How Can execute a lot of times the get request to obtain 0-100 progress? Thanks for your help!
Principal Component:
import axios from 'axios';
import React, { Fragment, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { transactionFileAction, uploadFileAction } from '../../actions/uploadFileAction';
import { RootStore } from '../../store';
import ProgressBar from '../progressBar/ProgressBar';
const UploadFile: React.FC = () => {
const dispatch = useDispatch();
const uploadFileStore = useSelector((state: RootStore) => state.uploadFile);
const [file, setFile] = useState<FormData>();
const [uploadProgress, setUploadProgress] = useState<number | undefined>(0);
const [uploadStatus, setUploadStatus] = useState<boolean>(false);
useEffect(() => {
if (uploadStatus) setUploadStatus(false);
}, [uploadStatus, uploadFileStore]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const formData = new FormData();
formData.append("file", e.target.files[0]);
setFile(formData);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
dispatch(uploadFileAction(file));
setUploadStatus(true);
};
return (
<Fragment>
<form onSubmit={(e) => handleSubmit(e)}>
<label>Notes</label>
<input
type="file"
name="file"
accept=".pdf,.doc,.docx,.xls,.xlsx"
onChange={(e) => handleChange(e)}
/>
<button type="submit">Agregar</button>
{uploadFileStore.UploadFileResponse?.message ? (
<p>{uploadFileStore.UploadFileResponse?.message}</p>
) : null}
</form>
{uploadFileStore.UploadFileResponse?.transactionId ? (
<ProgressBar
transactionId={uploadFileStore.UploadFileResponse?.transactionId}
/>
) : null}
</Fragment>
);
};
export default UploadFile;
Upload Progress Component:
import axios from 'axios';
import React, { useEffect, useState } from 'react';
interface Props {
transactionId: string | undefined;
}
const ProgressBar: React.FC<Props> = ({ transactionId }) => {
const [uploadProgress, setUploadProgress] = useState<number | undefined>(0);
console.log("entra");
useEffect(() => {
axios
.get(`http://localhost:3000/file_parser/${transactionId}`)
.then((response) => {
setUploadProgress(response.data.progress);
});
}, [transactionId, uploadProgress]);
return (
<div>
<p>{transactionId}</p>
<p>Progreso: {uploadProgress && uploadProgress}</p>
</div>
);
};
export default ProgressBar;
Since you need to make a request and this will not trigger re-render else one time since transaction will only call one time, then you can do that via use time interval for request to progress every 1 second for example...
ex:
let us has an useInterval custom hook:
import React, { useEffect, useRef } from 'react';
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember the latest callback.
savedCallback.current = callback;
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;
Then in my component we can call it:
useInterval(() => {
if(!transactionId) return;
axios
.get(`http://localhost:3000/file_parser/${transactionId}`)
.then((response) => {
setUploadProgress(response.data.progress);
});
}, 1000);
Another Option:
Base on result from api, we keep check if the progress response is 100 or not, if not then trigger it again...
for example:
const [forceUpdate, setForceUpdate] = useState(null);
useEffect(() => {
axios
.get(`http://localhost:3000/file_parser/${transactionId}`)
.then((response) => {
setUploadProgress(response.data.progress);
if(response.data.progress !== 100){
setForceUpdate(new Date());
}
});
}, [transactionId, forceUpdate]);
Another option
its work base on events nested of api request or Socket or any option...
I am creating a stopwatch in React.js and i am wondering why window.addEventListener('keydown', callback) re-render my component?
import { useEffect, useState } from 'react';
import './App.scss';
import Timer from './Timer';
import Button from './Button';
import Time from './Time';
const App = () => {
const [isRunning, setIsRunning] = useState(false);
const [start, setStart] = useState(new Time(0));
const [stop, setStop] = useState(new Time(0));
const handleStart = () => {
const now = new Date();
setIsRunning(true);
setStart(new Time(now));
setStop(new Time(now));
};
const handleStop = () => {
setIsRunning(false);
setStop(new Time(new Date()));
};
const getTime = () => {
if (isRunning) {
return new Time(new Date().getTime() - start.origin);
} else {
return new Time(stop.origin - start.origin);
}
};
const handleKeyDown = (key) => {
console.log(key.code === 'Space');
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div className="stopwatch">
<Timer getTime={getTime} />
<div className="buttons">
<Button role={'start'} callback={handleStart} />
<Button role={'stop'} callback={handleStop} />
</div>
</div>
);
};
export default App;
When i click start and then stop after let's say 3s. <Timer /> show correctly time that has passed, but then when i press Space on keyboard <Timer /> is re-rendering, showing new time. Then, when i switch my web-browser to VSCode and again to web-browser, <Timer /> isn't re-rendering
Here is my Timer component
import { memo, useEffect, useRef } from 'react';
const Timer = ({ getTime }) => {
const timer = useRef();
console.log('timer rendered');
useEffect(() => {
function run() {
const time = getTime().formatted();
timer.current.textContent = `${time.m}:${time.s}.${time.ms}`;
requestAnimationFrame(run);
}
run();
return () => {
cancelAnimationFrame(run);
};
});
return <div ref={timer} className="timer"></div>;
};
export default memo(Timer);
no matter if I use [] in both or none of useEffect nothing changes.
As #davood-falahati says, adding an empty array as a second argument to useEffect would probably be desirable. From the docs:
... If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works. ...
In your use case:
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
I have a CryptoCard component from "import {CryptoCard} from "react-ui-flex-cards""
It's placed in a StockCard component which uses material-ui too. And as I can see Material-ui' card component somehow overrides my component.
In HTML inspection I found this:
<div class="card crypto-card"> //this is my component
So as I see it CryptoCard component uses also the card class as css.
If in inspection I turn off background-color: #fafaf" it shows the CryptoCard component with the correct backgroundColor.
Here is my StockCard component
import Axios from 'axios';
import React,{useState, useEffect} from 'react';
import { makeStyles } from '#material-ui/core/styles';
import {CryptoCard} from "react-ui-flex-cards";
function StockCard(props) {
const [FetchInterval, setFetchInterval] = useState(300000);
const [StockData, setStockData] = useState({});
const [TrendDirection, setTrendDirection] = useState(0);
const [Trend, setTrend] = useState(0);
const classes = useStyles();
const FetchData = async () =>{
const resp = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
setStockData(resp.data);
}
const calculateTrendDirection = () => {
if(StockData.currentPrice > StockData.previousClosePrice){
setTrendDirection(1);
} else if (StockData.currentPrice < StockData.previousClosePrice){
setTrendDirection(-1);
} else {
setTrendDirection(0);
}
}
const calculateTrend = () => {
var result = 100 * Math.abs( ( StockData.previousClosePrice - StockData.currentPrice ) / ( (StockData.previousClosePrice + StockData.currentPrice)/2 ) );
setTrend(result.toFixed(2));
}
useEffect(() => {
FetchData();
const interval = setInterval(async() => {
await FetchData();
}, FetchInterval)
return() => clearInterval(interval);
},[FetchInterval]);
useEffect(()=>{
if(StockData.stock){
console.log("Trends calculated", StockData.name);
calculateTrend();
calculateTrendDirection();
}
},[StockData])
return(
<div>
<CryptoCard
currencyName={StockData.stock? StockData.stock.name : "Name"}
currencyPrice={StockData.stock? `$ ${StockData.currentPrice}` : 0}
icon={<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Bitcoin.svg/2000px-Bitcoin.svg.png"/>}
currencyShortName={StockData.stock? StockData.stock.symbol : "Symbol"}
trend={StockData.stock? `${Trend} %` : 0}
trendDirection={StockData.stock? TrendDirection : 0}
chartData={[9200, 5720, 8100, 6734, 7054, 7832, 6421, 7383, 8697, 8850]}
/>
</div>
)
}
export default StockCard;
With regular React it's possible to have something like this:
class NoteEditor extends React.PureComponent {
constructor() {
super();
this.state = {
noteId: 123,
};
}
componentWillUnmount() {
logger('This note has been closed: ' + this.state.noteId);
}
// ... more code to load and save note
}
In React Hooks, one could write this:
function NoteEditor {
const [noteId, setNoteId] = useState(123);
useEffect(() => {
return () => {
logger('This note has been closed: ' + noteId); // bug!!
}
}, [])
return '...';
}
What's returned from useEffect will be executed only once before the component unmount, however the state (as in the code above) would be stale.
A solution would be to pass noteId as a dependency, but then the effect would run on every render, not just once. Or to use a reference, but this is very hard to maintain.
So is there any recommended pattern to implement this using React Hook?
With regular React, it's possible to access the state from anywhere in the component, but with hooks it seems there are only convoluted ways, each with serious drawbacks, or maybe I'm just missing something.
Any suggestion?
useRef() to the rescue.
Since the ref is mutable and exists for the lifetime of the component, we can use it to store the current value whenever it is updated and still access that value in the cleanup function of our useEffect via the ref's value .current property.
So there will be an additional useEffect() to keep the ref's value updated whenever the state changes.
Sample snippet
const [value, setValue] = useState();
const valueRef = useRef();
useEffect(() => {
valueRef.current = value;
}, [value]);
useEffect(() => {
return function cleanup() {
console.log(valueRef.current);
};
}, []);
Thanks to the author of https://www.timveletta.com/blog/2020-07-14-accessing-react-state-in-your-component-cleanup-with-hooks/. Please refer this link for deep diving.
useState() is a specialized form of useReducer(), so you can substitute a full reducer to get the current state and get around the closure problem.
NoteEditor
import React, { useEffect, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "set":
return action.payload;
case "unMount":
console.log("This note has been closed: " + state); // This note has been closed: 201
break;
default:
throw new Error();
}
}
function NoteEditor({ initialNoteId }) {
const [noteId, dispatch] = useReducer(reducer, initialNoteId);
useEffect(function logBeforeUnMount() {
return () => dispatch({ type: "unMount" });
}, []);
useEffect(function changeIdSideEffect() {
setTimeout(() => {
dispatch({ type: "set", payload: noteId + 1 });
}, 1000);
}, []);
return <div>{noteId}</div>;
}
export default NoteEditor;
App
import React, { useState, useEffect } from "react";
import "./styles.css";
import NoteEditor from "./note-editor";
export default function App() {
const [notes, setNotes] = useState([100, 200, 300]);
useEffect(function removeNote() {
setTimeout(() => {
setNotes([100, 300]);
}, 2000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{notes.map(note => (
<NoteEditor key={`Note${note}`} initialNoteId={note} />
))}
</div>
);
}
I wanted to chime in with an answer for this in case someone else runs into this. If you need more than one value in your useEffect unmount function, it's important to make sure that the correct dependencies are being used. So the accepted answer works fine because it's just one dependency, but start including more dependencies, and it gets complicated. The amount of useRef's you need get out of hand. So instead, what you can do is a useRef that is the unmount function itself, and call that when you unmount the component:
import React, { useRef, useState, useContext, useCallback, useEffect } from 'react';
import { Heading, Input } from '../components';
import { AppContext } from '../../AppContext';
export const TitleSection: React.FC = ({ thing }) => {
const { updateThing } = useContext(AppContext);
const [name, setName] = useState(thing.name);
const timer = useRef(null);
const onUnmount = useRef();
const handleChangeName = useCallback((event) => {
setName(event.target.value);
timer.current !== null && clearTimeout(timer.current);
timer.current = setTimeout(() => {
updateThing({
name: name || ''
});
timer.current = null;
}, 1000);
}, [name, updateThing]);
useEffect(() => {
onUnmount.current = () => {
if (thing?.name !== name) {
timer.current !== null && clearTimeout(timer.current);
updateThing({
name: name || '',
});
timer.current = null;
}
};
}, [thing?.name, name, updateThing]);
useEffect(() => {
return () => {
onUnmount.current?.();
};
}, []);
return (
<>
<Heading as="h1" fontSize="md" style={{ marginBottom: 5 }}>
Name
</Heading>
<Input
placeholder='Grab eggs from the store...'
value={name}
onChange={handleChangeName}
variant='white'
/>
</>
);
};
I am set up a Timeout for when the user stops typing for 3 seconds an api call is made and the ActivityIndicator appears.
edited with full code:
import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ActivityIndicator } from 'react-native';
import {
Container,
SearchBar,
SearchBarInput,
SearchLoading,
SearchResultList,
Product,
} from './styles';
import api from '../../services/api';
export default function Search() {
const [searchResult, setSearchResult] = useState([]);
const [searchText, setSearchText] = useState('');
const [searching, setSearching] = useState(false);
const [focused, setFocused] = useState(false);
function renderProduct({ item }) {
return <Text>Oi</Text>;
}
let timer;
function handleChangeText(text) {
setSearching(false);
setSearchText(text);
clearTimeout(timer);
timer = setTimeout(() => setSearching(true), 3000);
}
useEffect(() => {
async function search() {
const response = await api.get(`products?search=${searchText}`);
setSearchResult(response.data);
setSearching(false);
}
if (searching) {
search();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searching]);
return (
<Container>
<SearchBar focused={focused}>
<SearchBarInput
placeholder="Pesquisar..."
onChangeText={handleChangeText}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
value={searchText}
/>
{searching && (
<SearchLoading>
<ActivityIndicator size="small" color="#000" />
</SearchLoading>
)}
</SearchBar>
<SearchResultList
data={searchResult}
keyExtractor={item => String(item.id)}
renderItem={renderProduct}
/>
</Container>
);
}
..............................................
But it's not working as it should:
https://user-images.githubusercontent.com/54718471/69919848-14680a00-1460-11ea-9047-250251e42223.gif
Remember that the body of the function is run on every single render. So the reference to the existing timer is lost each time the component re-renders.
You can use the useRef hook (https://reactjs.org/docs/hooks-reference.html#useref) to keep a stable reference across renders.
const timerRef = useRef(null);
function handleChangeText(text) {
setSearching(false);
setSearchText(text);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setSearching(true), 3000);
}