I'm trying to build a simple timer that will start and stop on click.
all my project is functional component based (using recompose), so I'm not sure where to set the setInterval.
here is a thing I tried to play with until I was completely lost where to store the setInterval so I'll be able to clear it on onStop fn (that will fire on a button) - as in functional component there is no this that I can put the timer and remove it from ... what's the functional components way of doing it ?
https://codepen.io/anon/pen/jQQZrm?editors=0010
any suggestions ?
- using react-native
thanks.
You need here 3 different state handlers: stopTimer, startTimer and updateValue(I've used slightly different naming than your code did).
In startTimer you need to create timer that runs updateValue by timer. In other words you need to call indirectly one state handler from another.
There is no way doing that. But. You can split those handlers into 2 sets: "value + updateValue" and "stopTimer + startTimer + intervalId". Then you will be able to get state handlers from first set in second as props:
const EnchanceApp = compose(
withStateHandlers({
timer: 0,
}, {
updateValue: ({timer}) =>
() => ({timer: timer + 1})
}),
withStateHandlers({
timerId: 0,
}, {
startTimer: ({timerId}, {updateValue}) =>
() => {
clearInterval(timerId);
return {
timerId: setInterval(updateValue, 1000)
};
},
stopTimer: ({timerId}) =>
() => clearInterval(timerId)
})
)(App);
works perfect, my code sample:
const BgList = ({ bgs }) => (
<PoseGroup>
{bgs.map(item => <StyledBg key={item} style={{backgroundImage: 'url(/img/'+item+'.jpg)'}} />)}
</PoseGroup>
);
const enhance = compose(
withStateHanlders(
() => ({
index: 0,
isVisible: false,
bgs: _.shuffle([0,1,2,3]),
timerId: 0,
}),
{
startTimer: () => ({timerId}, {updateValue}) => {
clearInterval(timerId);
return {
timerId: setInterval(updateValue, 5000)
};
},
stopTimer: ({timerId}) => () => clearInterval(timerId),
updateValue: ({bgs}) =>
() => {
return ({bgs: _.shuffle(bgs)})
},
},
),
lifecycle({
componentDidMount() {
const {timerId, updateValue} = this.props;
this.props.startTimer({timerId}, {updateValue})
}
}),
)
const BlockAnimated = enhance(({
bgs
}) => {
return (
<BgList bgs={bgs} />
Related
I'm trying to call function once after component created with react.. I'm new in to this, so hope you can help me to understand. I tryied to put onLoad event in component creation, but it doesn't work. I tried to just put function call, but it's being called in the circle, but i need it to be called once - when component will finish to load.
Below is the function i got to create component and i want function 'handleClick' also to be called when component will be loaded.
function BadgeSample(props) {
const {
type,
defaultValue,
className,
style,
value,
bootstrapStyle,
clickable,
onClickAction,
getRef
} = props;
const [disabled, setDisabled] = React.useState(false);
const [pasuedd, setPasued] = React.useState(false);
const [highlightSection, setHighlightSection] = React.useState({
from: 0,
to: 0
});
const synth = window.speechSynthesis;
let utterance;
const handleClick = () => {
if (!synth) {
console.error("no tts");
return;
}
utterance = new SpeechSynthesisUtterance(value || defaultValue);
utterance.addEventListener("start", () => setDisabled(true));
utterance.addEventListener("end", () => setDisabled(false));
utterance.addEventListener("boundary", ({
charIndex,
charLength
}) => {
setHighlightSection({
from: charIndex,
to: charIndex + charLength
});
});
synth.speak(utterance);
};
const stoped = () => {
synth.cancel(utterance);
};
const pasued = () => {
setPasued(true);
synth.pause(utterance);
};
const resumed = () => {
setPasued(false);
synth.resume(utterance);
};
//handleClick(); - here it will work, but it will repeat itself in the loop and never stopped
return React.createElement("div", {
className: "App",
onClick: onClickAction,
onLoad: handleClick, // - here it doesn't work..
ref: getRef,
style: style
}, React.createElement(HighlightedText, _extends$sj({
text: value || defaultValue
}, highlightSection)), React.createElement("button", {
className: disabled ? "stopbtn" : "playbtn",
onClick: disabled ? stoped : handleClick
}, disabled ? React.createElement("div", null, React.createElement(StopCircleFill, null), "Stop") : React.createElement("div", null, React.createElement(PlayCircleFill, null), "Listen")), disabled ? React.createElement("button", {
className: pasuedd ? "playbtn" : "pausebtn",
onClick: pasuedd ? resumed : pasued
}, pasuedd ? React.createElement("div", null, React.createElement(PlayCircleFill, null), "Resume") : React.createElement("div", null, React.createElement(PauseCircleFill, null), "Pause")) : "");
}
I try to make one call of function after page with component will be loaded.. But it doesn't work.
you can use useEffect.
its the most efficiant way that i know and it's very comfortable to use.
its a function that you can decide when you call it, and you have an option to call it just once when your website renders the first time.
here's an example:
import React, { useEffect } from 'react';
const YourComponent = () => {
useEffect(() => {
// your function here
}, []);
return (
// your component's content and so... );
};
and if you want it to be called when one of you states changes you can do that too.
I highly recommend you read about it and use it for your projects.
hope it helps :)
Using React for practice, I'm trying to build a small notification system that disappears after a given period using timeouts.
A sample scenario;
User creates one notification and wait for it's to expire
The cleanup function runs from useEffect and clears the timeout.
This would be no problem and clears out the only available timeout. The problem appears when I'm adding more:
Render #1 - adding the first notification
Render #2 - the cleanup function calls from render #1 for adding a new notification. This adds a new notification but clears a timeout before it's done.
The timeout expires from render #2 so runs the cleanup function and clears the right timeout.
It's a fairly simple component, which renders a array of objects (with the timeout in it) from a Zustand store.
export const Notifications = () => { const { queue, } = useStore()
useEffect(() => {
if (!queue.length || !queue) return
// eslint-disable-next-line consistent-return
return () => {
const { timeout } = queue[0]
timeout && clearTimeout(timeout)
} }, [queue])
return (
<div className="absolute bottom-0 mb-8 space-y-3">
{queue.map(({ id, value }) => (
<NotificationComponent key={id} requestDiscard={() => id}>
{value}
</NotificationComponent>
))}
</div> ) }
My question is; is there any way to not delete a running timeout when adding a new notification? I also tried finding the last notification in the array by queue[queue.length - 1], but it somehow doesn't make any sense
My zustand store:
interface State {
queue: Notification[]
add: (notification: Notification) => void
rm: (id: string) => void
}
const useNotificationStore = c<State>(set => ({
add: (notification: Notification) =>
set(({ queue }) => ({ queue: [...queue, notification] })),
rm: (id: string) =>
set(({ queue }) => ({
queue: queue.filter(n => id !== n.id),
})),
queue: [],
}))
My hook for adding notifications;
export function useStoreForStackOverflow() {
const { add, rm } = useNotificationStore()
const notificate = (value: string) => {
const id = nanoid()
const timeout = setTimeout(() => rm(id), 2000)
return add({ id, value, timeout })
}
return { notificate }
}
I think with a minor tweak/refactor you can instead use an array of queued timeouts. Don't use the useEffect hook to manage the timeouts other than using a single mounting useEffect to return an unmounting cleanup function to clear out any remaining running timeouts when the component unmounts.
Use enqueue and dequeue functions to start a timeout and enqueue a stored payload and timer id, to be cleared by the dequeue function.
const [timerQueue, setTimerQueue] = useState([]);
useEffect(() => {
return () => timerQueue.forEach(({ timerId }) => clearTimeout(timerId));
}, []);
const enqueue = (id) => {
const timerId = setTimeout(dequeue, 3000, id);
setTimerQueue((queue) =>
queue.concat({
id,
timerId
})
);
};
const dequeue = (id) =>
setTimerQueue((queue) => queue.filter((el) => el.id !== id));
Demo
You can use useRef here to track all timeouts in a Map.
The cool thing about Map is that you can use your Notification as key and the timer as value.
I created a small POC with useState but it should also work fine with Zustand:
https://codesandbox.io/s/young-worker-hg38x
import { useEffect, useRef, useState } from "react";
export default function App() {
const [notifications, setNotification] = useState<{message: string}[]>([]);
const timers = useRef(new Map());
useEffect(() => {
const newNotifications = notifications.filter((notification) => !timers.current.has(notification));
newNotifications.forEach((newNotification) => {
timers.current.set(newNotification, setTimeout(() => {
// Remove notification
setNotification((currentNotifications) => currentNotifications.filter((notification) => notification !== newNotification));
// Remove notification from timer tracker:
timers.current.delete(newNotification)
}, 3000));
});
}, [notifications])
// Clear all timers on unmount
useEffect(() => {
return () => {
[...timers.current.values()].forEach((timer) => {
clearTimeout(timer);
})
}
}, [])
return (
<div className="App">
{notifications.map((notification) => <p>{notification.message}</p>)}
<button onClick={() => {
setNotification([...notifications, {
message: 'notification' + Math.random()
}]);
}}>add</button>
</div>
);
}
I have two functions in React Native Component, in that one should refresh every 10s and another one should refresh every 1s. I have implemented setInterval() function for refreshing on componentDidMount() and clearInterval() on componentWillUnmount(), but am facing trouble it takes only one function which has the lowest duration. But am achieving result if set duration of both function same duration.
Here is the example
...
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
btLevel: 0,
btState: null,
};
}
componentDidMount() {
this.getBtLevels();
this.getBtState();
this.batLS2();
this.batLS10();
}
componentWillUnmount() {
clearInterval(() => { this.batLSS(); this.batLS10(); });
}
getBtLevels = () => {
fetch(apiUrl).then((res) =>
this.setState({btLevel: res.level}),
);
};
getBtLevelArcs = () => {
fetch(apiUrl).then((res) =>
this.setState({btLevelArc: res.level}),
);
};
getBtState = () => {
fetch(apiUrl).then((res) =>
this.setState({BtState: res.state}),
);
};
batLS10 = () => {
setInterval(() => {
this.getBtLevelArcs();
}, 10000);
};
batLS2 = () => {
setInterval(() => {
this.getBtLevels();
this.getBtState();
}, 1000);
};
...
On the above Code this.getBtLevels(); this.getBtState(); fetch value every 1 seconds and this.getBtLevelArcs(); fetch value every 10 secounds. In this this.getBtLevels(); this.getBtLevelArcs(); functions get same value. But one should refresh every 1 second and another one every 10 seconds. Here am getting is 1s setInterval function this.batLS2() is refresh whole component.
How can I achieve this one should refresh value 1s and another 10s.
here is the Original Version code. Expo
Issue
clearInterval works by being passed the reference returned from setInterval, i.e. this.timerId = setInterval(... and clearInterval(this.timerId).
What I suspect is occurring is you edited you code and ran, which set an interval callback (but didn't clear it), and then edited and re-ran your code, which sets another interval callback (again, not cleared), etc... You basically aren't cleaning up the interval callbacks when the component unmounts (like a page refresh).
Solution
Add a timer variable for each interval timer
constructor(props) {
super(props);
...
this.timer1 = null;
this.timer2 = null;
}
Clear each interval on dismount
componentWillUnmount() {
clearInterval(this.timer1)
clearInterval(this.timer2)
}
Save the timer ref
batLS10 = () => {
this.timer2 = setInterval(() => {
this.getBtLevelArcs();
}, 10000);
};
batLS2 = () => {
this.timer1 = setInterval(() => {
this.getBtLevels();
this.getBtState();
}, 1000);
};
What I understood by the example and statement is you want getBtLevels and getBtState to be called after every 1 sec and getBtLevelArcs after every 10 seconds.
But what happens is when getBtState and getBtLevels invoke, their setState updates the whole component, which is not acceptable in your case.
Ideally this should not be a problem, because all the three functions have different states. btLevel, btLevelArc and btState. Updating one state should not impact the other one. But that totally depends upon your UI logic.
If that is still a problem: what you can do. You can split your component into two components. First one will hold the UI related to getBtLevels and getBtState and second component will contain UI related to getBtLevelArcs. This is required because setState will re-render the whole component.
Code will be something like this:
class App extends React.Component {
...
//some common handlers for SubApp1 and SubApp2
...
render() {
return (
<React.Fragment>
<SubApp1 />
<SubApp2 />
</React.Fragment>
)
}
class SubApp1 extends React.Component {
constructor(props) {
super(props);
this.state = {
btLevel: 0,
btState: null,
};
}
componentDidMount() {
this.getBtLevels();
this.getBtState();
this.batLS2();
}
componentWillUnmount() {
clearInterval(() => { this.batLSS(); });
}
getBtLevels = () => {
fetch(apiUrl).then((res) =>
this.setState({ btLevel: res.level }),
);
};
getBtState = () => {
fetch(apiUrl).then((res) =>
this.setState({ BtState: res.state }),
);
};
batLS2 = () => {
setInterval(() => {
this.getBtLevels();
this.getBtState();
}, 1000);
}
...
...
class SubApp2 extends React.Component {
constructor(props) {
super(props);
this.state = {
btLevelArc: 'some default value'
};
}
componentDidMount() {
this.batLS10();
}
componentWillUnmount() {
clearInterval(() => { this.batLS10(); });
}
getBtLevels = () => {
fetch(apiUrl).then((res) =>
this.setState({ btLevel: res.level }),
);
};
getBtState = () => {
fetch(apiUrl).then((res) =>
this.setState({ BtState: res.state }),
);
};
getBtLevelArcs = () => {
fetch(apiUrl).then((res) =>
this.setState({ btLevelArc: res.level }),
);
};
...
...
I'm using a React Context to store data and to provide functionality to modify these data.
Now, I'm trying to convert a Class Component into a Functional Component using React Hooks.
While everything is working as expected in the Class, I don't get it to work in the Functional Component.
Since my applications code is a bit more complex, I've created this small example (JSFiddle link), which allows to reproduce the problem:
First the Context, which is the same for both, the Class and the Functional Component:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
constructor (props) {
super(props);
this.increase = this.increase.bind(this);
this.reset = this.reset.bind(this);
this.state = {
current: 0,
increase: this.increase,
reset: this.reset
}
}
render () {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase (step) {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset () {
this.setState({
current: 0
});
}
}
Now, here is the Class component, which works just fine:
class MyComponent extends React.Component {
constructor (props) {
super(props);
this.increaseByOne = this.increaseByOne.bind(this);
}
componentDidMount () {
setInterval(this.increaseByOne, 1000);
}
render () {
const count = this.context;
return (
<div>{count.current}</div>
);
}
increaseByOne () {
const count = this.context;
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}
}
MyComponent.contextType = MyContext;
The expected result is, that it counts to 5, in an interval of one second - and then starts again from 0.
And here is the converted Functional Component:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, []);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Instead of resetting the counter at 5, it resumes counting.
The problem is, that count.current in line if (count.current === 5) { is always 0, since it does not use the latest value.
The only way I get this to work, is to adjust the code on the following way:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, [count]);
React.useEffect(() => {
console.log('useEffect');
const interval = setInterval(increaseByOne, 1000);
return () => {
clearInterval(interval);
};
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Now, the increaseByOne callback is recreated on every change of the context, which also means that the effect is called every second.
The result is, that it clears the interval and sets a new one, on every change to the context (You can see that in the browser console).
This may work in this small example, but it changed the original logic, and has a lot more overhead.
My application does not rely on an interval, but it's listening for an event. Removing the event listener and adding it again later, would mean, that I may loose some events, if they are fired between the remove and the binding of the listener, which is done asynchronously by React.
Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?
I've created a fiddle here, to play around with the code above:
https://jsfiddle.net/Jens_Duttke/78y15o9p/
First solution is to put data is changing through time into useRef so it would be accessible by reference not by closure(as well as you access actual this.state in class-based version)
const MyComponent = (props) => {
const countByRef = React.useRef(0);
countByRef.current = React.useContext(MyContext);
React.useEffect(() => {
setInterval(() => {
const count = countByRef.current;
console.log(count.current);
if (count.current === 5) {
count.reset();
} else {
count.increase(1);
}
}, 1000);
}, []);
return (
<div>{countByRef.current.current}</div>
);
}
Another solution is to modify reset and increase to allow functional argument as well as it's possible with setState and useState's updater.
Then it would be
useEffect(() => {
setInterval(() => {
count.increase(current => current === 5? 0: current + 1);
}, 1000);
}, [])
PS also hope you have not missed clean up function in your real code:
useEffect(() => {
const timerId = setInterval(..., 1000);
return () => {clearInterval(timerId);};
}, [])
otherwise you will have memory leakage
If the increaseByOne function doesn't need to know the actual count.current, you can avoid recreating it. In the context create a new function called is that checks if the current is equal a value:
is = n => this.state.current === n;
And use this function in the increaseByOne function:
if (count.is(5)) {
count.reset();
}
Example:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase = (step) => {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset = () => {
this.setState({
current: 0
});
}
is = n => this.state.current === n;
state = {
current: 0,
increase: this.increase,
reset: this.reset,
is: this.is
};
}
const MyComponent = (props) => {
const { increase, reset, is, current } = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
if (is(5)) {
reset();
} else {
increase(1);
}
}, [increase, reset, is]);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{current}</div>
);
}
const App = () => (
<MyContextProvider>
<MyComponent />
</MyContextProvider>
);
ReactDOM.render( <
App / > ,
document.querySelector("#app")
);
body {
background: #fff;
padding: 20px;
font-family: Helvetica;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>
quick summary
I'm trying to create a button that has both a regular click and a separate action that happens when a user clicks and holds it, similar to the back button in Chrome.
The way I'm doing this involves a setTimeout() with a callback that checks for something in state. For some reason, the callback is using state from the time that setTimeout() was called, and not at the time when it's callback is called (1 second later).
You can view it on codesandbox
how I'm trying to accomplish this
In order to get this feature, I'm calling setTimeOut() onMouseDown. I also set isHolding, which is in state, to true.
onMouseUp I set isHolding to false and also run clickHandler(), which is a prop, if the hold function hasn't had time to be called.
The callback in setTimeOut() will check if isHolding is true, and if it is, it will run clickHoldHandler(), which is a prop.
problem
isHolding is in state (I'm using hooks), but when setTimeout() fires it's callback, I'm not getting back the current state, but what the state was when setTimetout() was first called.
my code
Here's how I'm doing it:
const Button = ({ clickHandler, clickHoldHandler, children }) => {
const [isHolding, setIsHolding] = useState(false);
const [holdStartTime, setHoldStartTime] = useState(undefined);
const holdTime = 1000;
const clickHoldAction = e => {
console.log(`is holding: ${isHolding}`);
if (isHolding) {
clickHoldHandler(e);
}
};
const onMouseDown = e => {
setIsHolding(true);
setHoldStartTime(new Date().getTime());
setTimeout(() => {
clickHoldAction(e);
}, holdTime);
};
const onMouseUp = e => {
setIsHolding(false);
const totalHoldTime = new Date().getTime() - holdStartTime;
if (totalHoldTime < holdTime || !clickHoldHandler) {
clickHandler(e);
}
};
const cancelHold = () => {
setIsHolding(false);
};
return (
<button
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={cancelHold}
>
{children}
</button>
);
};
You should wrap that callback task into a reducer and trigger the timeout as an effect. Yes, that makes things certainly more complicated (but it's "best practice"):
const Button = ({ clickHandler, clickHoldHandler, children }) => {
const holdTime = 1000;
const [holding, pointer] = useReducer((state, action) => {
if(action === "down")
return { holding: true, time: Date.now() };
if(action === "up") {
if(!state.holding)
return { holding: false };
if(state.time + holdTime > Date.now()) {
clickHandler();
} else {
clickHoldHandler();
}
return { holding: false };
}
if(action === "leave")
return { holding: false };
}, { holding: false, time: 0 });
useEffect(() => {
if(holding.holding) {
const timer = setTimeout(() => pointer("up"), holdTime - Date.now() + holding.time);
return () => clearTimeout(timer);
}
}, [holding]);
return (
<button
onMouseDown={() => pointer("down")}
onMouseUp={() => pointer("up")}
onMouseLeave={() => pointer("leave")}
>
{children}
</button>
);
};
working sandbox: https://codesandbox.io/s/7yn9xmx15j
As a fallback if the reducer gets too complicated, you could memoize an object of settings (not best practice):
const state = useMemo({
isHolding: false,
holdStartTime: undefined,
}, []);
// somewhere
state.isHolding = true;