How to emit side effect using new state directly after updating state - reactjs

I have some code like this:
const [_channel, ably] = useChannel(channelName, msg => {
if (msg.type) === type1 {
setStateVariable(prev => new_from_prev(prev));
// Need the updated state for this
if (condition(stateVariable)) {
// This line attempts to send a socket message
sendMessage(/* someData */);
}
}
})
useChannel is a wrapper around useEffect with dependency array [channelName]; you can see the source code here: https://github.com/ably-labs/react-hooks/blob/main/src/hooks/useChannel.ts
I am not able to access the updated state this way because setStateVariable does the state update asynchronously. This does not work with refs either for the same reason. I am aware that I can use the callback to setStateVariable, however there are several problems with this. First, it violates seperation of concerns and is clearly an anti-pattern. Second, in React Strict Mode, setStateVariable will run twice, sending my socket message twice, which I don't want. The only solution I can think of is putting the code that needs to access the updated state into a setTimeout with an arbitrary timeout length. I have tried this and it works. However, this is obviously a hack and not reliable. There must be a better way to do this. Any help?

Related

RxJS and repeated events

I am new to RxJs in general but am investigating a bug in some React code in which, upon an unrelated action, an old event seems to be emitted and rendered to a display error. Think if you had two buttons that generated two messages somewhere on screen, and clicking one button was showing the message for the other button.
Being new to RxJs I'm not positive where the problem lays. I don't see a single ReplaySubject in the code, only Obserables, Subjects, and BehaviourSubjects. So this is either misuse of an RxJs feature or just some bad logic somewhere.
Anyway I found the code with the related Observable and I'm not quite sure what this person was trying to accomplish here. I have read up on combineLatest, map, and pipe, but this looks like pointless code to me. Could it also be somehow re-emitting old events? I don't see dynamic subscriptions anywhere, especially in this case.
Tldr I don't understand the intent of this code.
export interface IFeedback {
id: number
text: string
}
export interface IFeedbackMessages {
message: IFeedback | undefined
}
feedback$ = new BehaviorSubject<IFeedback | undefined>(undefined)
feedbackNotifs$: Observable<IFeedbackMessages> = combineLatest([
feedback$
]).pipe(
map(([feedback]) => ({
feedback
})
))
I also found this which maybe be an issue. In the React component that displays this message, am I wrong but does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
const FeedbackDisplay: React.FC () => {
const [feedbackNotifications, setFeedbackNotifications] = React.useState<IFeedbackMessages>()
React.useEffect(() =>
{
const sub = notification$.subscribe(setFeedbackNotifications)
return () => sub?.unsubscribe()
}, [notifications$])
}
Could it also be somehow re-emitting old events?
Yes, it probably is. BehaviorSubject has the unique property of immediately emitting the last value pushed to it as soon as you subscribe to it.
It's great when you want to model some persistent state value, and it's not good for events whose actual moment of occurrence is key. It sounds like the feedback messages you're working with fall into the second category, in which case Subject is probably a better choice.
does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
Not exactly. useEffect accepts a callback, and within that callback you can optionally return a "cleanup" function. React will hang onto that function until the effect is triggered again, then it calls it to clean things up (which in this case consists of closing out the subscription) to make room for the next effect.
So in this case, the unsubscribe will only happen when the component is rendered with a new value for notifications$. Also worth pointing out that notifications$ will only change if it's either passed as a prop or created within the component function. If it's defined outside the function (imported from another file for example), you don't need to (and in fact should not) put it into useEffect's dependency array.

React hooks: am I doing an anti pattern? updating state in function outside the component

I am beginning to use React (hooks only), and facing a strange issue. I am trying to reproduce the problem in a small test code, but can't get it to happen, except in my full blown app. This leads me to wonder if I'm doing something really wrong.
I have an array of objects, declared as a state. I map this array to display its content. Except that nothing gets displayed (the array is filled, but nothing gets displayed). Now if I declare an un-related state, make it a boolean which flips each time my array gets updated, then my array gets displayed properly. As if, in the render phase itself, React did not detect the array's changes.
A few things:
the array gets updated by a socketIO connection, I simulate it here with a timer
I update my array OUTSIDE of my component function, BUT providing the setter function to the update function
I also create part of the render fields outside my component function (this has no effect, just for readability in my full app)
I essence, this is what I am doing:
const updateArray = (setTestArray, setTestTag, addArray) => {
setTestArray(prevTestArray => {
let newTestArray = prevTestArray.map((data, index) => (data + addArray[index]))
return newTestArray
})
setTestTag(prevTag => {
return (!prevTag)
})
}
const renderArray = (currentTestArray) => {
return currentTestArray.map((data, index) => (
<div>
testArray[{index}]={data}
</div>
))
}
function TestPage(props) {
const [testArray, setTestArray] = useState([])
const [testTag, setTestTag] = useState(false)
useEffect(() => {
let samples = 3
let initArray= []
for (let i=0; i<samples;i++) initArray[i] = Math.random()
setTestArray(initArray)
// In real code: setup socket here...
setInterval(() => {
let addArray= []
for (let i=0; i<samples;i++) addArray[i] = Math.random()
updateArray(setTestArray, setTestTag, addArray)
}, 1000)
return (() => {
// In real code, disconnect socket here...
})
}, []);
return (
<Paper>
Array content:
{renderArray(testArray)}
<br/>
Tag: {(testTag)? 'true' : 'false'}
</Paper>
)
}
This works just fine. But, in my full app, if I comment out everything concerning "testTag", then my array content never displays. testArray's content is as expected, updates just fine, but placing a debugger inside the map section show that array as empty.
Thus my questions:
is my updateArray function a bad idea? From what I read, my prevTestArray input will always reflect the latest state value, and setTestArray is never supposed to change... This is the only way I see to handle the async calls my socket connection generate, without placing "testArray" in my useEffect dependencies (thus avoiding continuously connecting/disconnecting the socket?)
rendering outside the component, in renderArray, doesn't affect my tests (same result if I move the code inside my component), but is there a problem with this?
As a side note, my array's content is actually more complex is the real app (array of objects), I have tried placing this in this test code, it works just fine too...
Thank you!
Edit: Note that moving updateArray inside the useEffect seems to be the recommended pattern. I did that in my full app. The hook linter does not complain about any missing dependency, yet this still doesn't work in my full app.
But the question is still whether what I am doing here is wrong or not: I know it goes against the guidelines as it prevents the linter from doing its job, but it looks to me like this will still work, the previous state being accessible by default in the setter functions.
Edit #2: Shame on me... silly mistake in my real app code, the equivalent of updateArray had a shallow array copy at some place instead of a deep copy.
Why adding the flipping tag made it work is beyond me (knowing the data was then indeed properly displayed and updated), but getting rid of this mistake solved it all.
I will leave this question on, as the question still stand: is placing the state update, and part of the rendering outside the component a functional problem, or just something which might mater on hide dependencies (preventing the react hooks linter from doing its job) and thus simply bad practice?
The fact is that things work just fine now with both functions outside the component function, which makes sense based on what I understand from hooks at this point.

Reselect: how to cache data written to a reducer?

I try to reselect and immediately got up the problem: data polling works. Almost every time the same data arrives in the reducer — a component rerender occurs using this data. I try to cache this data with the selector — fails — the rerender still happens. What is wrong with this code?
function getAllTickets(reducer) {
return reducer.get('tickets');
}
export const allTicketsSelector = createSelector([getAllTickets], items => items);
By default, the surface comparison function is used in createSelector. Want to make a deep comparison, use createSelectorCreator and your comparison function. There is an example in the documentation.

Is there a better way of updating the tx hash?

Following is a snippet of my code (which is working)
....
let txhash = '';
class App extends Component {
....
async componentDidMount() {
.....
}
onSubmit = async(event) => {
event.preventDefault();
const accounts = await web3.eth.getAccounts();
this.setState({
message: "Waiting for the transaction to be mined..."
});
await lottery.methods.enterLotteryContract().send({
from: accounts[0],
value: web3.utils.toWei(this.state.value, 'ether')
}).on('transactionHash', function(hash) {
txhash = hash
});
this.setState({
message: "Congrats, the tx has been mined; your txhash is " + txhash
})
console.log(txhash);
});
}
More over here: https://github.com/DKSukhani/lottery-react/blob/master/src/App.js
Was wondering if there is a better way for me to get the txhash and then display it. I was hoping that I could make txhash as one of the state components rather than declaring it as an empty variable. Also I am using the call back function in only reassigning it to txhash.
Can you ask your question a bit more clearly? Your code makes sense and looks fine for some unknown purpose. That purpose is exactly what I need to know though.
When do you need the txHash, is it after every call of this function or are you trying to track something else.
Why can't it be a state variable, you didn't include state in your snipped so I had to go look at the rest of your code and see no reason why not. You use it within the snippet so it's clear you can use it to some degree. I don't use React so maybe there's some render loop, listener issue, or other reason you can't use setState.
With that said, check out this link on the different events you can listen to with on, as I think you're listening to the wrong event or outputting the wrong text. When you set the state's message to say that the tx was mined I don't believe it actually has been.
I would use confirmation and only display the message on the first fire of the event, then ignore the remaining 23. The object returned alongside the confirmation number is described here and has a transactionHash variable you can use to both check that the transaction was confirmed and get the hash at the same time. Now your event call is no longer as wasteful and you can use the same setState call for the message as for the txHash, or even skip storing the txHash at all (as long as you can still use setState asynchronously).

How do I chain state updates?

I have a form with multiple controls that saves everything to a variable. Each control has an onChanged function, which runs a state update with that control's new value:
function onChangedValUpdate(newVal){
let fields = clone(this.state.fields);
fields[controlId] = newVal;
this.setState({fields});
}
My controls are dynamically created, and when they are, they run onChangedValUpdate on their initial value, if one is present. The problem is, sometimes a lot of controls are created at once, and React queues up its setStates using the same cloned fields object for each update. The object is not updated between setStates, presumably for similar reasons to this question. This means that, effectively, all but one control's updates are overwritten.
I tried writing an over-smart routine which used setState's callback to only run it if there isn't one already in progress and remember changes made to the fields variable in between setStates, but React went and ran all my queued updates simultaneously. Regardless, the routine felt too contrived to be right.
I'm sure this is a trivial and solved problem, but I can't seem to formulate my question in a Googleable way. How do I chain state updates that happen concurrently, and of which there may be any number?
EDIT For posterity, my solution, thanks to Yuri:
function onChangedValUpdate(newVal){
this.setState( state => {
let fields = clone(state.fields);
fields[controlId] = newVal;
return {fields};
}
}
You could pass a mutation function to setState. This will prevent overwritting on batched updates because every callback will get the most recent previous state.
function onChangedValUpdate(newVal){
this.setState(function(state){
const fields = clone(state.fields)
fields[controlId] = newVal
return {fields: fields}
});
}
Or using object spread and enhanced object literals.
function onChangedValUpdate(newVal){
this.setState(({fields}) => ({fields: {...fields, [controlId]: newVal}}));
}

Resources