Accept promises in asyncOptions alongside callbacks#496
Accept promises in asyncOptions alongside callbacks#496JedWatson merged 2 commits intoJedWatson:masterfrom
asyncOptions alongside callbacks#496Conversation
|
I don't think it's a good idea to decide for people that they'll need to bring in a hefty promises library just to use |
|
They don't need any promise library if they don't use You probably fetch your asynchronous options from some endpoint, using http calls that returns promises (unless they're blocking). We could easily test for |
It's working for plenty of people as it is, no?
All in all, I'm not sure if I get the argument here. We can't tell people not using a huge ajax implementation that they're out of luck because we assume everyone is using promises. If added complexity is the worry and the // what's proposed
return fetch(`some/uri/with/${query}`)
.then((response) => {
return response.json();
});
// all that's needed w/o promise support
return fetch(`some/uri/with/${query}`)
.then((response) => {
callback(null, response.json());
}) |
|
I'm happy to add Promise support, given the upcoming Fetch API works with them and it's a popular pattern, but not at the cost of callback support. For one, this would be a major and unnecessary (imo) breaking change for a lot of current users; it also, as @brianreavis mentions, means that - for a while at least - including a Promise library would be necessary to use I was thinking about whether we should support a different prop for the Promise Wouldn't this do the trick? |
|
Fair enough @brianreavis, you do have a point. @JedWatson something like that could work, but we also need to provide a way to cache the results with I'll update the PR later today, cheers |
|
Thanks @dmatteo 👍 |
|
Ok, I really need some help here, as I don't know exactly how to proceed. I have got I have a working example here: https://github.com/dmatteo/react-select/tree/asyncExamples. However, some tests are failing on the caching, as apparently the function gets called more than necessary. p.s. @JedWatson you sure we don't want a new prop? :) |
src/Select.js
Outdated
There was a problem hiding this comment.
Do you mean to be calling asyncOptions() three times like this? If you follow @JedWatson's example, the behavior of react-select with and without promises shouldn't be any different.
There was a problem hiding this comment.
I would love not to call it three times like this, as I agree it's pretty bad.
Unfortunately @JedWatson approach is not enough here, since asyncOptions it's a function that returns a promise, not a promise itself, in order to be able to take an input.
How do you check if a function returns a promise without calling the function itself?
There was a problem hiding this comment.
You call this.props.asyncOptions(input) and assign the result to a variable. Then check the variable instead of subsequent calls to this.props.asyncOptions().
Basically:
let result = this.props.asyncOptions(input);
if (result !== 'undefined' &&
typeof result.then === 'function') {
result.then((data) => {
There was a problem hiding this comment.
But to be clear, you need to also support the callback case here, so be sure to pass the callback argument to asyncOptions when you invoke it:
let result = this.props.asyncOptions(input, callback);
(haven't looked at how else you'd structure your code to allow this, I'm out and about at the moment but if you need more help I'll check back later)
There was a problem hiding this comment.
You call this.props.asyncOptions(input) and assign the result to a variable.
Meh, of course 😴
But to be clear, you need to also support the callback case here,
Yep, I am calling it in https://github.com/dmatteo/react-select/blob/asyncOptions/src/Select.js#L594-L596, but I'm not passing it in the promise.
|
Ok, the I'm saving the return value of the first asyncOptions call (the callback one) and after that has being executed I check if the result is a promise and then execute the promise logic. The only downside I see is that if users provide a function that returns a promise and also accepts a callback with 2 params, it will run both the callback and the promise paths. What do you think? |
|
As soon as I have positive feedback I'll write a few tests for this. |
|
@dmatteo looks good to me. To challenge you though: how about we reduce code duplication & abstract the handling of promise return vs. the callback? If you define the callback before assigning |
src/Select.js
Outdated
There was a problem hiding this comment.
You're re-calling this.props.asyncOptions(input) rather than just using asyncOpts.then
|
@JedWatson I overlooked the fact that the callback was exactly the same, thanks! I'll add some tests now |
|
@JedWatson I've added some tests (more to come) and I'm using an internal var to keep track of the number of calls and the input that is passed in (dmatteo@4d1673e#diff-f227bbc93e56e86e165bcdf5e07e8ea4R1389) Since I'm not using spies there, it seemed like a good way to go. What do you think? |
|
@dmatteo looks good to me, I'm going to defer to our test master @bruderstein though in case he's got any feedback on that :) |
|
I definitely can't live up to that title 😄 Is there any reason why you're not using spies there? That may make it easier and more readable (including the assertions) - just wrap your function with asyncOptions = sinon.spy((input) => {
const options = [
{ value: 'test', label: 'TEST one' },
{ value: 'test2', label: 'TEST two' },
{ value: 'tell', label: 'TELL three' }
].filter((elm) => {
return (elm.value.indexOf(input) !== -1 || elm.label.indexOf(input) !== -1);
});
return new Promise((resolve) => {
resolve({options: options});
})
}
);Then your test for calling etc can use the built in assertions it('should be called once at the beginning', () => {
expect(asyncOptions, 'was called once');
});(You get a nicer error message if it fails too) The three tests that test your asyncOptions mock, I'd personally put in a nested describe('[mocked asyncOptions]', () => {
it('should fulfill asyncOptions promise', () => {
...
});
it('should fulfill with 3 options when asyncOptions promise when input = "te"', () => {
...
});
it('should fulfill with 2 options when asyncOptions promise when input = "tes"', () => {
...
});
});This is just a style thing though, it just took me a moment to realise what the tests were trying to prove. Then the last test that tests calling the asyncOptions ( it('calls the asyncOptions again when the input changes', () => {
typeSearchText('ab');
expect(asyncOptions, 'was called with', 'ab');
expect(asyncOptions, 'was called times', 2);
});You get a much nicer error message if it fails here too, especially if it wasn't called with the right arguments, you get to see what it was called with. I'd actually add two further tests here, and one of them is probably the most important for this change, in that we need to check that it actually updates the options with what the asyncOptions returns in the promise (ie. calls If you add another In theory, you can grab the promise from the return value in the spy, and then just add your it('updates the options after the promise returns', () => {
typeSearchText('te');
return asyncOptions.firstCall.returnValue.then(() => {
expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option',
'to satisfy', [
expect.it('to have text', 'TEST one'),
expect.it('to have text', 'TEST two'),
expect.it('to have text', 'TELL three')
]);
});
});The second test I'd add, would be one where the promise rejects. I'm not quite sure what to expect if that happens, I guess we'd just check that the previous options weren't changed? So maybe return a resolved promise on the first call, then a rejected on the second, and check the options are still the same as after the first promise resolved. Sorry for the long comment - hope it's helpful, just my 2 cents. |
|
Hey @bruderstein, thanks a lot for your very thorough comment!
No reason if not my inability to make it work with spies. I had tried different approaches, and none of them worked, but again, I didn't know that you could wrap a function in a spy like that.
Makes sense, I'll do that before the "actual" tests As for testing the outcome of the resolved promise in the DOM, that's was what I still had to wrap my head around. Thanks again and I'll ping you back when I have something Cheers |
|
Thanks @bruderstein! Great write-up. @vitalbone lots of good info here you may be interested in too re: writing tests for Keystone |
|
Ofc, I'll squash and rebase appropriately when we're done 😄 |
|
ping @JedWatson @bruderstein |
|
Sorry, been busy with other stuff :( Looks good - I had a thought one night but not had time to have a proper look, that maybe we could simplify the mock with a stub that just returns a fixed promise based on the value, but it's up to you. const asyncOptions = sinon.stub();
asyncOptions.withArgs('').returns(Promise.resolve({ options: [ /* ... default options */]}))
asyncOptions.withArgs('te').returns(Promise.resolve({ options: [ /* ... te options */] }));
asyncOptions.withArgs('tes').returns(Promise.resolve({ options: [ /*... tes options */] }));
asyncOptions.withArgs('fail').returns(Promise.reject(new Error('something is wrong, jim')));Which would save the tests on the mock, and actually having any logic in the mock. I've not tried this out - not sure it's "better", but either way, the tests now are looking good! Sorry for the delay in feedback :( |
|
No worries for the delay :) I'm not entirely sure that the "full stubs" version is better than the mocked promise though. What do you think? |
|
@bruderstein reping, sorry :) |
|
I'll leave this to you - both are reasonable approaches, and both are pretty understandable. |
|
Alrighty, I'd rather keep it like this then. @JedWatson I've squashed and rebased, we should be ready to go! |
asyncOptions instead of callbacksasyncOptions alongside callbacks
|
@JedWatson ping? |
|
Sorry @dmatteo! missed the notification when you wrapped this up. Good to go. It's nearly 2am here so I'll publish tomorrow when I'm awake :) |
Accept promises in `asyncOptions` alongside callbacks
|
and thank you for your work on this! |
|
Eheh no worries. Thanks to you for the great component! |
This PR is addressing the inability of
asyncOptionsto handle promises, as discussed in #491I'm keeping commits separated by concerns (component, searchingText tests, searchPrompt tests) for easy review.
Before moving on to the rest of the failing tests, I would like to hear what you think about
asyncOptionsonly being able to handle promises from now on.