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.
Related
The kinda the same problem as described here
How to compare oldValues and newValues on React Hooks useEffect?
But in my case, usePrevious hook does not help.
Imagine the form with several inputs, selects, and so on. You may want to look at https://app.uniswap.org/#/swap to make a similar visualization. There are several actions and data updates that will be happened on almost any change, which will lead to several re-renders, at least 4. For example.
I have 2 inputs, each represents a token. Base(first one) and Quote(second one).
This is a state for Base
const [base, setBase] = useState({
balance: undefined,
price: undefined,
value: initState?.base?.value,
token: initState?.base?.token,
tokenId: initState?.base?.tokenId,
});
and for Quote
const [quote, setQuote] = useState({
balance: undefined,
price: undefined,
value: initState?.quote?.value,
token: initState?.quote?.token,
tokenId: initState?.quote?.tokenId,
});
They gonna form a pair, like BTC/USD for example.
By changing token (instead of BTC I will choose ETH) in the select menu I will trigger several actions: fetching wallet balance, fetching price, and there are gonna be a few more rerenders with input view update and modal window close. So at least 4 of them are happening right now. I want to be able to compare base.token and basePrv with
const basePrv = usePrevious(base?.token); but on the second re-render base.token and basePrv gonna have the same token property already and it is an issue.
I also have the swap functionality between the inputs where I should change base with quote and quote with base like that
setBase(prevState => ({
...prevState,
base: quote
}));
setQuote(prevState => ({
...prevState,
quote: base
}));
In that case, there are no additional requests that should be triggered.
Right now I have useEffect with token dependency on it. But it will be fired each time when the token gonna be changed which will lead to additional asynchronous calls and 'tail' of requests if you gonna click fast. That's why I need to compare the token property that was before the change to understand should I make additional calls and requests because of the formation of new pair (BTC/USD becomes ETH/USD) or I should ignore that because it was just a "swap" (BTC/USD becomes USD/BTC) and there is no need to make additional calls and fetches. I just had to, well, swap them, not more.
So in my story, usePrevious hook will return the previous token property only once, and at the second and third time, it would be overwritten by multiple re-renders(other properties would be fetched) to the new one. So at the time when useEffect gonna be triggered, I would have no chance to compare the previous token property and the current one, because they will show the same.
I have several thoughts on how to solve it, but I am not sure is it right or wrong, because it seemed to me that the decisions look more imperative than declarative.
I can leave everything as it is (requests would be triggered always on any change no matter what it was. Was it a swap or user changed a pair). I can disable the swap button until all of the requests would be finished. It would solve the problem with requests 'tail'. But it is a kinda hotfix, that gonna be work, but I do not like it, because it would lead to additional unnecessary requests and it would be slow and bad for UX.
I can use a state to keep the previous pair on it right before the update by setBase or setQuote happens. It will allow me to use useEffect and compare previous pair to the current one to understand did the pair was changed, or just swapped and take the decision should I make fetches and calls or not.
I can get rid of useEffect with base.token and quote.token dependencies and handle everything inside of onClick handler. Because of that, the swap functionality would not trigger useEffect, and calls and fetches would be fired only if the user gonna click and choose something different. But as I said this option seemed a little bit odd to me.
I tried to use closure here, to "remember" the previous state of tokens, but it is kinda similar to use the current component state. Also, you have to initialize closure outside of the functional component body, and I do not see a possibility to transfer the init state into it that way, so the code becomes more spaghettified.
So any other ideas guys? I definitely missing something. Maybe that much of re-renders is an antipattern but I am not sure how to avoid that.
There could be multiple solutions to your problem. I would suggest to pick one which is easier to understand.
1. Modify the usePrevious hook
You can modify the usePrevious hook to survive multiple renders.
Tip: use JSON.stringify to compare if you think the value will be a complex object and might change the reference even for same real value.
function usePrevious(value) {
const prevRef = useRef();
const curRef = useRef();
if (value !== curRef.current){
// or, use
// if ( JSON.stringify(value) !== JSON.stringify(curRef.current)){
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
2. Sort useEffect dependency array
Since you're using tokens(strings) as dependency array of useEffect, and you don't mind their order (swap shouldn't change anything), sort the dependency array like
useEffect(
() => {
// do some effect
},
[base.token, quote.token].sort()
)
3. Store the currently fetched tokens.
While storing the API response data, also store the tokens(part of request) associated with that data. Now, you'll have 2 sets of tokens.
currently selected tokens
currently fetched tokens
You can chose to fetch only when the currently fetched tokens don't fulfil your needs. You can also extend this and store previous API request/responses and pick the result from them if possible.
Verdict
Out of all these, 3rd seems a nice & more standardised approach to me, but an overkill for your need (unless you want to cache previous results).
I would have gone with 2nd because of simplicity and minimalism. However, It still depends on what you find easier at the end.
I've just started using Recoil on a new project and I'm not sure if there is a better way to accomplish this.
My app is an interface to basically edit a JSON file containing an array of objects. It reads the file in, groups the objects based on a specific property into tabs, and then a user can navigate the tabs, see the few hundred values per tab, make changes and then save the changes.
I'm using recoil because it allows me to access the state of each input from anywhere in my app, which makes saving much easier - in theory...
In order to generate State for each object in the JSON file, I've created an component that returns null and I map over the initial array, create the component, which creates Recoil state using an AtomFamily, and then also saves the ID to another piece of Recoil state so I can keep a list of everything.
Question 1 Is these a better way to do this? The null component doesn't feel right, but storing the whole array in a single piece of state causes a re-render of everything on every keypress.
To Save the data, I have a button which calls a function. That function just needs to get the ID's, loop through them, get the state of each one, and push them into an Array. I've done this with a Selector too, but the issue is that I can't call getRecoilValue from a function because of the Rules of Hooks - but if I make the value available to the parent component, it again slows everything right down.
Question 2 I'm pretty sure I'm missing the right way to think about storing state and using hooks, but I haven't found any samples for this particular use case - needing to generate the state up front, and then accessing it all again on Save. Any guidance?
Question 1
Get accustomed to null-rendering components, you almost can't avoid them with Recoil and, more in general, this hooks-first React world š
About the useRecoilValue inside a function: you're right, you should leverage useRecoilCallback for that kind of task. With useRecoilCallback you have a central point where you can get and set whatever you want at once. Take a look at this working CodeSandbox where I tried to replicate (the most minimal way) your use-case. The SaveData component (a dedicated component is not necessary, you could just expose the Recoil callback without creating an ad-hoc component) is the following
const SaveData = () => {
const saveData = useRecoilCallback(({ snapshot }) => async () => {
const ids = await snapshot.getPromise(carIds);
for (const carId of ids) {
const car = await snapshot.getPromise(cars(carId));
const carIndex = db.findIndex(({ id }) => id === carId);
db[carIndex] = car;
}
console.log("Data saved, new `db` is");
console.log(JSON.stringify(db, null, 2));
});
return <button onClick={saveData}>Save data</button>;
};
as you can see:
it retrieves all the ids through const ids = await snapshot.getPromise(carIds);
it uses the ids to retrieve all the cars from the atom family const car = await snapshot.getPromise(cars(carId));
All of that in a central point, without hooks and without subscribing the component to atoms updates.
Question 2
There are a few approaches for your use case:
creating empty atoms when the app starts, updating them, and saving them in the end. It's what my CodeSandbox does
doing the same but initializing the atoms through RecoilRoot' initialState prop
being updated by Recoil about every atom change. This is possible with useRecoilTransactionObserver but please, note that it's currently marked as unstable. A new way to do the same will be available soon (I guess) but at the moment it's the only solution
The latter is the "smarter" approach but it really depends on your use case, it's up to you to think if you really want to update the JSON at every atom' update š
I hope it helps, let me know if I missed something š
I'm creating my own simple snackbar/toast stacker. However, I'm having problems with queing them in an orderly manner. Removing a snackbar from the snackbar que causes re-render and odd behavior.
The basic flow:
Click a button which causes the addSnack function to fire which is provided by the withSnackbar HOC.
Take the parameters from the fired function, and create a snack accordingly and add it to the snackbar list.
At the end, we render the snackbar list.
Each snackbar controls it's own appearance and disappearance, and is controlled by a time out. After the timeout is fired, it calls removeSnack function which is suppose to remove the first snack from the list.
codesandbox
If you click the button for example, four times in a short amount of time. They render nicely, but when the first one is to be deleted, they all disappear and reappear abnormally.
I understand that it's partially the state re-renderings fault, however, I'm not sure how to handle it in a way that the removal is handled gracefully without affecting the rendering of other snacks.
So, after many hours of trial and error, I found a solution that works so far. Moving and reading the snacks outside of the state helped with the bizarre rendering problems, and with it, I was able to create a message que which works well.
Working example
Codesandbox
If you look at splice document, you will notice that it's returning an array of deleted elements and not the initial array.
You can correct it by splicing then updating:
snacks.splice(-1, 1);
addSnacks(snacks);
However you are still going to have some weird behavior and you might need to use a keyed list to fix that.
i had the same issue and i saw your solution, but i was really trying to find out why it happens - here is why:
when u call a useState hook from an async function's callback, you should use the callback format of the hook to make sure that you are working with the latest value. example:
const [messages, setMessages] = useState([]);
const addMessage = ( message ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
return [ ...prevMessages, message ];
});
};
const removeMessage = ( index ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
let newMessages = [...prevMessages];
newMessages.splice( index, 1 );
return newMessages;
});
};
I have a problem and I'm pretty sure I'm not the only one who ever had it... Although I tried to find a solution, I didin't really find something that fits my purpose.
I won't post much code, since its not really a code problem, but more a logic problem.
Imagine I have the following hook:
useEffect(() => {
fetchFromApi(props.match.params.id);
}, [props.match.params.id]);
Imagine the result of fetchFromApi is displayed in a simple table in the UI.
Now lets say the user clicks on an entity in the navigation, so the ID prop in the browser URL changes and the effect triggers, leading to an API call. Lets say the call with this specific ID takes 5 seconds.
During this 5 seconds, the user again clicks on an element in the navigation, so the hook triggers again. This time, the API call only takes 0,1 seconds. The result is immediatly displayed.
But the first call is still running. Once its finished, it overwrites the current result, what leads to wrong data being displayed in the wrong navigation section.
Is there a easy way to solve this? I know I can't cancel promises by default, but I also know that there are ways to achieve it...
Also, it could be possible that fetchFromApi is not a single API call, but instead multiple calls to multiple endpoints, so the whole thing could become really tricky...
Thanks for any help.
The solution to this is extremely simple, you just have to determine whether the response that you got was from the latest API call or not and only then except it. You can do it by storing a triggerTime in ref. If the API call has been triggered another time, the ref will store a different value, however the closure variable will hold the same previously set value and it mean that another API call has been triggered after this and so we don't need to accept the current result.
const timer = useRef(null);
useEffect(() => {
fetchFromApi(props.match.params.id, timer);
}, [props.match.params.id]);
function fetchFromApi(id, timer) {
timer.current = Date.now();
const triggerTime = timer.current;
fetch('path').then(() => {
if(timer.current == triggerTime) {
// process result here
// accept response and update state
}
})
}
Other ways to handle such scenarios to the cancel the previously pending API requests. IF you use Axios it provides you with cancelToken that you can use, and similarly you can cancel XMLHttpRequests too.
While React with Redux is excellent at modeling UI state, there are occasionally situations where something just happens, the UI needs to handle that event in a discrete procedural way, and it doesnāt make sense to think of that transient event as a piece of state that would persist for any period of time.
Two examples, from a JS Bin-like code editor application:
A user exports their code to a GitHub gist. When the export is complete, we want to open a new browser window displaying the gist. Thus, the React component hierarchy needs to know the ID of the gist, but only at a single moment in time, at which point it will open the window and stop caring about the gist export altogether.
A user clicks on an error message, which causes the editor to bring the line where the error occurred into focus in the editor. Again, the UI only cares about which line needs to be focused for a single moment in time, at which point the (non-React-based) editor is told to focus the line, and the whole thing is forgotten about.
The least unsatisfying solution Iāve come up with is:
When the triggering event occurs, dispatch an action to update the Redux state with the needed information (gist ID, line to focus)
The React component thatās interested in that information will monitor the appropriate props in a lifecycle hook (componentWillReceiveProps etc.). When the information appears in its props, it takes the appropriate action (loads the gist window, focuses the editor on the line)
The component then immediately dispatches another event to the Redux store essentially saying, āIāve handled thisā. The transient event data is removed from Redux state.
Is there a better pattern for this sort of situation? I think one perhaps fundamental part of the picture is that the UIās response to the action always breaks out of the React component structureāopening a new window, calling a method on the editorās API, etc.
Your solution would certainly work, but these sorts of problems you bring up don't sound particularly well suited to be handled with redux at all. Just using plain React and passing the necessary functions to your component sounds a lot more natural to me.
For the export case, for instance, rather than dispatching an action which updates some state, which then triggers the new window to open, why not just open the new window in place of dispatching that action? If you have the info necessary to dispatch an action to trigger opening a window, you ought to be able to just open the window in the same place.
For the example where clicking an error message triggers calling a non-React, imperative api, pass a function from the nearest common parent of the error message and the editor. The parent can also maintain a ref to the wrapper around the editor. Even if it's multiple levels deep, it's not too bad to get a ref to what you want if you pass down a function to set the ref. Thus, the function passed down from the parent to the error message component can simply call a method on the ref it maintains to the editor. Basically, something like this:
class Parent extends Component {
constructor(...args) {
super(...args)
this.onErrorMessageClick = this.onErrorMessageClick.bind(this)
}
onErrorMessageClick(lineNumber) {
if (this.editor) {
this.editor.focusOnLine(lineNumber)
}
}
render() {
return (
<div>
<ErrorMessage onClick={ this.onErrorMessageClick } lineNumber={ 1 } />
<ErrorMessage onClick={ this.onErrorMessageClick } lineNumber={ 2 } />
<EditorWrapper editorRef={ (editor) => { this.editor = editor } } />
</div>
)
}
}
const ErrorMessage = ({ onClick, lineNumber }) => (
<button onClick={ () => onClick(lineNumber) }>
{ `Error Message For ${lineNumber}` }
</button>
)
// Just adding EditorWrapper because your editor and error messages probably aren't right next to each other
const EditorWrapper = ({ editorRef }) => <Editor ref={ editorRef } />
class Editor extends Component {
focusOnLine(lineNumber) {
// Tell editor to focus on the lineNumber
}
render() {
...
}
}
I often use redux-thunk for these types of events.
Essentially, its not different to setting up a normal thunk, only I don't dispatch an action in it.
const openGist = (id) => () => {
// code to open gist for `id`
}
I then use this action creator like I would any other from the component triggering it, e.g. mapped in mapDispatchToProps and called in an onClick handler.
A common question I get asked is why don't I just put this code straight into the component itself, and the answer is simple - testability. It is far easier to test the the component if it doesn't have events that cause side effects and it's easier to test code that cause side effects in isolation to anything else.
The other advantage is that more often than not, the UX designers will step in at some point and want some kind of feedback to the user for some of the events of this nature (e.g. briefly highlight the error row) so adding an X_COMPLETED action to the event is much easier at this point.