-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Include switchMap
operator?
#52
Comments
Yeah, the question of ordering is a little more interesting for observables than for iterators. I might honestly prefer to omit |
In this implementation, |
The |
Agreed, switchMap is one of the most important operators for doing a common (and very difficult by hand) ordering of multiple async operations. (I think I disagree on dropping |
For context, my advice to people when trying to figure out which flattening operation to choose in RxJS is basically: "default to
If folks would like input.on('input').switchMap(async () => {
const res = await fetch(`/lookahead?q=${input.value}`);
if (!res.ok) {
console.log('bad lookahead')
}
return res.json();
})
.subscribe({
next: data => {
results.innerHTML = data
.map(d => `<li><button type="button" tabIndex="0">${d}</button></li>`)
.join('\n');
}
}) |
Isn't |
This is off topic, but just to share: At worst, In almost a decade, I've never seen The reason I say that
Consider this: document.on('click')
.filter(e => e.target.matches('button.cancel-order'))
.switchMap(e =>
fetch('/order-cancel', {
method: 'POST'
body: e.target.dataset.id,
})
.then(resp => resp.json())|
)
.subscribe(deleted => {
document.querySelector(`.order[data-id="${deleted.id}"]`).remove();
}) With If you switch that with That said, |
Your first example kind of scares me as written - the right thing is to cancel the One wild idea for that: the mapper (in this and potentially other callback-taking methods) could take as its third argument a input.on('input')
.switchMap(async (e, idx, { signal }) => {
const response = await fetch(`/search?q={e.target.value}`, { signal });
/* ... */
})
.subscribe((results) => { /* ... */ }) and have the |
@bakkot I understand your concern, however, given the design of Naive approachThis is the sort of thing most people will opt for today. In part because the dance around let currentSearch = '';
searchInput.addEventListener('input', async (e) => {
const searchText = e.target.value;
if (searchText !== currentSearch) {
currentSearch = searchText;
const response = await fetch(`/search?q={searchText)`);
if (currentSearch !== searchText) return;
if (response.ok) {
const results = await response.json();
if (currentSearch !== searchText) return;
resultsList.innerHTML = results
.map(result => `<li><a href="${result.href}">${result.text}</a></li>`)
.join('');
}
}
}); Raw AbortSignal ApproachThis is what you'd have to do today to do things the "correct" way using let ac;
let currentSearch = '';
searchInput.addEventListener('input', async (e) => {
const searchText = e.target.value;
ac?.abort();
if (searchText !== currentSearch) {
currentSearch = searchText;
ac = new AbortController();
try {
const response = await fetch(`/search?q={searchText)`, { signal: ac.signal });
if (response.ok) {
const results = await response.json();
resultsList.innerHTML = results
.map(result => `<li><a href="${result.href}">${result.text}</a></li>`)
.join('');
}
} catch (error) {
if (error?.name !== 'AbortError') {
throw error;
}
}
}
}); Observables With Naive
|
Yup, I was about to type up a similar (tho much less detailed) response - the lack of a switchMap makes an even more wrong implementation the easiest thing to do, with worse results for the user. Thinking about how we can make this pattern even better is very useful, but we shouldn't discard it just because it's not perfect. The pattern it's addressing will be written anyway, just wrongly. |
Edit: I wrote a thing here which I think was wrong given how |
OK, so, what do you actually write if you have observables, but not let ac;
let currentSearch = '';
searchInput.on('input')
.map(e => e.target.value)
.filter(searchText => searchText !== currentSearch)
.map(async (searchText) => {
ac?.abort();
ac = new AbortController();
try {
currentSearch = searchText;
const response = await fetch(`/search?q={searchText)`, { signal: ac.signal });
if (response.ok) {
return await response.json();
}
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
})
.flatMap(x => x)
.filter(v => v !== void 0)
.subscribe((results) => {
if (results) {
resultsList.innerHTML = results
.map(result => `<li><a href="${result.href}">${result.text}</a></li>`)
.join('');
}
}) ? |
@bakkot you can always join (EDIT: Nevermind, you can't in this case because of the dance around getting cancellation to work to make it like That said, yes to emulate Then, if you're using an let ac;
let currentSearch = '';
searchInput.on('input')
.map(e => e.target.value)
.filter(searchText => {
if (searchText !== currentSearch) {
currentSearch = searchText;
ac?.abort(); // This can't happen inside the `flatMap`!
return true;
}
return false;
})
.flatMap(async function* (searchText) {
ac = new AbortController();
try {
const response = await fetch(`/search?q=${searchText}`, { signal: ac.signal });
if (response.ok) {
yield await response.json();
}
} catch (error) {
if (error.name !== 'AbortError') {
throw error;
}
}
})
.subscribe((results) => {
if (results) {
resultsList.innerHTML = results
.map(result => `<li><a href="${result.href}">${result.text}</a></li>`)
.join('');
}
}) |
Given the subtlety of getting the right behavior with I still think it would be nice to have the automatic |
The explainer includes
flatMap
, but notswitchMap
. I thinkswitchMap
should be included because it is essential in a common pattern:A simple example is a search bar that triggers a network request, but should cancel the previous one when the user types too quickly.
I don't think I have ever used the rxjs
flatMap
operator, but I'm usingswitchMap
for the pattern describe above all the time.From @benlesh: I created a duplicate issue (#90) for this accidentally, so I'll include the use cases and text I posted there here:
switchMap
was mentioned at TPAC as being an API of interest. It's a method that is especially useful for composing events in the browser because of its inherent cancellation. If API bloat is a concern, I would gladly removetoArray()
,some()
,find()
et al, as those are minimally useful with observables.Use cases are things like:
Use Case: Lookahead Search
In the below scenario, the result of the
fetch
would simply be ignored.It's plausible that we could provide a signal along with the
switchMap
callback, as well. In fact, I'd almost encourage that, as it might make fetch
more useful. But I think we'd want to discuss the ergonomics there, and that could
be debated and added later. (For example, we might want to have it automatically
swallow DOMExceptions named
AbortError
?)Use Case: Changing Connections
Below, every time a URL is changed in an URL input, if it's a valid URL,
it will disconnect from the previous web socket and connect to a new one.
The text was updated successfully, but these errors were encountered: