React - useEffect not using updated value in websocket onmessage - react-hooks

I have a simple issue where a state value updates in my code but is not using the new value. Any ideas what I can do to adjust this?
const [max, setMax] = useState<number>(10);
useEffect(() => {
console.log('max', max); //This outputs correct updated value.
ws.onmessage = (message: string => {
console.log('max', max); //This is always 10.
if (max > 100) {
doSomething(message);
}
}
},[]);
function onChange() {
setMax(1000);
}
<Select onChange={onChange}></Select> //this is abbrev for simplicity

Your useEffect is running only once, on the initial render, using the values from initial render, so max variable is closure captured and not updated in any way. But the solution will be pretty simple, using useRef and one more useEffect to update the ref variable when max variable updates.
const maxRef = useRef(10); // same value
const [max, setMax] = useState(10);
// Only used to update ref variable
useEffect(() => {
maxRef.current = max;
}, [max]);
useEffect(() => {
console.log("max", maxRef.current);
ws.onmessage = (message) => {
console.log("max", maxRef.current);
if (maxRef.current > 100) {
doSomething(message);
}
};
}, []);

Related

React - How can I improve my fetching like button data method

I often use this code to fetch and update data for my like button. It works but I wonder if there is a more effective or cleaner way to do this function.
const isPressed = useRef(false); // check the need to change the like count
const [like, setLike] = useState();
const [count, setCount] = useState(count_like); // already fetch data
const [haveFetch, setHaveFetch] = useState(false); // button block
useEffect(() => {
fetchIsLike(...).then((rs)=>{
setLike(rs);
setHaveFetch(true);
})
return () => {}
}, [])
useEffect(()=>{
if(like) {
// animation
if(isPressed.current) {
setCount(prev => (prev+1));
// add row to database
}
}
else {
// animation
if(isPressed.current) {
setCount(prev => (prev-1));
// delete row from database
}
}
}, [like])
const updateHeart = () => {
isPressed.current = true;
setLike(prev => !prev);
}

Why does my timer hook not update it's internal state?

For some reason my timer is not updating it's internal Timer State after I modify the input field. Here is the intial state of my page and State.
This is what my screen and state look like after I modify the input from 10 to 8 seconds. Notice that the Timer State does not update
Here is my code for the workout page:
function WorkoutPage(props: any) {
const DEFAULT_SECONDS_BETWEEN_REPS: number = 10
const [secondsBetweenRepsSetting, setSecondsBetweenRepsSetting] = useState(DEFAULT_SECONDS_BETWEEN_REPS)
const {secondsLeft, isRunning, start, stop} = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
})
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(secondsBetweenRepsSettingString)
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt)
}
return <React.Fragment>
<input type="number" name="secondsBetweenRepsSetting" value={secondsBetweenRepsSetting} onChange={onTimeBetweenRepsChange}/>
</React.Fragment>
}
Here is my useTimer Class:
import { useState } from 'react';
import Validate from "../utils/Validate";
import useInterval from "./useInterval";
export default function useTimer({ duration: timerDuration, onExpire, onTick}) {
const [secondsLeft, setSecondsLeft] = useState(timerDuration)
const [isRunning, setIsRunning] = useState(false)
function start() {
setIsRunning(true)
}
function stop() {
setIsRunning(false)
}
function handleExpire() {
Validate.onExpire(onExpire) && onExpire();
}
useInterval(() => {
const secondsMinusOne = secondsLeft - 1;
setSecondsLeft(secondsMinusOne)
if(secondsMinusOne <= 0) {
setSecondsLeft(timerDuration) // Reset timer automatically
handleExpire()
} else {
Validate.onTick(onTick) && onTick();
}
}, isRunning ? 1000 : null)
return {secondsLeft, isRunning, start, stop, }
}
My full codebase is here in case someone is interested: https://github.com/kamilski81/bdt-coach
Here's the sequence of events you expect:
User changes the input
The change handler fires and calls setSecondsBetweenRepsSetting with the new value
The component re-renders with the new value for secondsBetweenRepsSetting
useTimer is invoked with a duration property of the new value
The secondsLeft state in the useTimer hook changes to the new duration value <-- oops! this does not happen
Why doesn't this last item happen? Because within the useTimer implementation, the only place you use the duration is as the initial value of secondsLeft. Calling the hook a second time with a new duration value will not change the secondsLeft state, and this is by design.
My recommendation would be to include setSecondsLeft in the return value of the useTimer hook to give you a way to override the time left in the timer. You could then use setSecondsLeft directly in the input change handler:
const { secondsLeft, setSecondsLeft, isRunning, start, stop } = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
});
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(
secondsBetweenRepsSettingString
);
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt);
setSecondsLeft(secondsBetweenRepsSettingInt);
};

The countdown timer goes to negative

My countdown timer won’t stop after 0 and it went to negative even after I clear the interval. I seem not able to see where it went wrong.
Also after the timer goes to 0, I want the page automatically go to the next page without giving specific route, so I’m thinking using useHistory and goForward() but don’t know where I add the hook in this function. Can I return clearInterval and history.goForward() both?
import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
const Timer = () => {
const [seconds, setSeconds] = useState(10);
const history = useHistory();
useEffect(() => {
const interval = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000,
);
if (seconds === 0) {
return () => clearInterval(interval);
}
}, []);
return <div className="countdown">{seconds}</div>;
};
export default Timer;
Your useEffect function is only called once on first render and never again. So you don't actually clear the interval. clearInterval needs to sit outside of that function so that it can be called when the seconds reach zero. I would write your code like so:
export default function App() {
const [seconds, setSeconds] = React.useState(10);
const interval = React.useRef();
React.useEffect(() => {
interval.current = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000
);
}, []);
if (seconds === 0) {
clearInterval(interval.current);
}
return <div className="countdown">{seconds}</div>;
}
useRef is like a “box” that can hold a mutable value in its .current property.
So here you assign the interval to it and can clear it any time you want.
Sandbox
when you set timer inside useEffect it's not aware of changes. I have updated your code. codepen
export default function App() {
let [seconds, setSeconds] = useState(15),
[timer, setTimer] = useState(null); // IN YOU NEED TO STOP TIMER
useEffect(() => {
if (seconds > 0) updateSeconds();
else {
// go to next page
// set timer
// setSeconds(15);
}
// eslint-disable-next-line
}, [seconds]);
/* Timer Logic */
function updateSeconds() {
let timeOut = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
setTimer(timeOut);
}
return (
<div className="App">
<h1>Hello Timer</h1>
<h2>{seconds}</h2>
</div>
);
}

Re-execute async RxJS stream after delay

I'm using RxJS 6 to lazily step through iterable objects using code similar to example running below. This is working well but I'm having trouble solving my final use case.
Full code here
import { EMPTY, defer, from, of } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator (iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => result.done ? EMPTY : of(result.value))
);
}
function iterateValues ({ params }) {
const { values, delay: delayMilliseconds } = params;
const isIterable = typeof values[Symbol.iterator] === "function";
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
if (isIterable && delayMilliseconds > 0) {
const iterator = values[Symbol.iterator]();
// The first value is emitted immediately, the rest are emitted after time.
return stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
);
} else {
return from(values);
}
}
const options = {
params: {
// Any iterable object is walked manually. Otherwise delegate to `from()`.
values: ["Mary", "had", "a", "little", "lamb"],
// Delay _between_ values.
delay: 350,
// Delay before the stream restarts _after the last value_.
runAgainAfter: 1000,
}
};
iterateValues(options)
// Is not repeating?!
.pipe(repeat(3))
.subscribe(
v => {
console.log(v, Date.now());
},
console.error,
() => {
console.log('Complete');
}
);
I'd like to add in another option which will re-execute the stream, an indefinite number of times, after a delay (runAgainAfter). I'm having trouble composing this in cleanly without factoring the result.done case deeper. So far I've been unable to compose the run-again behavior around iterateValues.
What's the best approach to accomplish the use case?
Thanks!
Edit 1: repeat just hit me in the face. Perhaps it means to be friendly.
Edit 2: No, repeat isn't repeating but the observable is completing. Thanks for any help. I'm confused.
For posterity here is the full code sample for a revised edition is repeat-able and uses a consistent delay between items.
import { concat, EMPTY, defer, from, interval, of, throwError } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator(iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => (result.done ? EMPTY : of(result.value)))
);
}
function iterateValues({ params }) {
const { values, delay: delayMilliseconds, times = 1 } = params;
const isIterable =
values != null && typeof values[Symbol.iterator] === "function";
if (!isIterable) {
return throwError(new Error(`\`${values}\` is not iterable`));
}
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
const observable =
delayMilliseconds > 0
? defer(() => of(values[Symbol.iterator]())).pipe(
mergeMap(iterator =>
stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
)
)
)
: from(values);
return observable.pipe(repeat(times));
}
I'm gonna be honest, but there could be better solution for sure. In my solution, I ended up encapsulating delay logic in a custom runAgainAfter operator. Making it an independent part, that doesn't affect your code logic directly.
Full working code is here
And the code of runAgainAfter if anybody needs it:
import { Observable } from "rxjs";
export const runAgainAfter = delay => observable => {
return new Observable(observer => {
let timeout;
let subscription;
const subscribe = () => {
return observable.subscribe({
next(value) {
observer.next(value);
},
error(err) {
observer.error(err);
},
complete() {
timeout = setTimeout(() => {
subscription = subscribe();
}, delay);
}
});
};
subscription = subscribe();
return () => {
subscription.unsubscribe();
clearTimeout(timeout);
};
});
};
Hope it helps <3

How to return from within an observer?

I was trying to return filter function but return doesn't seem to work with callbacks. Here this.store.let(getIsPersonalized$) is an observable emitting boolean values and this.store.let(getPlayerSearchResults$) is an observable emiting objects of video class.
How do I run this synchronously, can I avoid asynchronus callback altogether given that I can't modify the observables received from store.
isPersonalized$ = this.store.let(getIsPersonalized$);
videos$ = this.store.let(getPlayerSearchResults$)
.map((vids) => this.myFilter(vids));
myFilter(vids) {
this.isPersonalized$.subscribe((x){
if(x){
return this.fileterX(vids);//Return from here
}
else {
return this.filterY(vids);//Or Return from here
}
});
}
fileterX(vids) {
return vids.filter((vid) => vids.views>100;);
}
fileterY(vids) {
return vids.filter((vid) => vids.views<20;);
}
I got it working this way, you don't need myFilter(vids) at all if you can get the branching out on isPersonalized$'s subscribe. Here is the updated code.
this.store.let(getIsPersonalized$);
videos$: Observable<any>;
ngOnInit() {
this.isPersonalized$.subscribe((x) => {
if (x) {
this.videos$ = this.store.let(getPlayerSearchResults$)
.map((vids) => this. fileterX(vids));
} else {
this.videos$ = this.store.let(getPlayerSearchResults$)
.map((vids) => this. fileterY(vids));
}
});
}
fileterX(vids) {
return vids.filter((vid) => vids.views>100;);
}
fileterY(vids) {
return vids.filter((vid) => vids.views<20;);
}
It looks like you want to evaluate the latest value of isPersonalized$ within the map function, i'd do that via withLatestFrom (Example: The first one toggles true/false every 5s, the second emits an increasing number every 1s):
const isPersonalized$ = Rx.Observable.interval(5000)
.map(value => value % 2 === 0);
const getPlayerSearchResults$ = Rx.Observable.interval(1000)
.withLatestFrom(isPersonalized$)
.map(bothValues => {
const searchResult = bothValues[0];
const isPersonalized = bothValues[1];
...
});

Resources