Issue with creating a reselect selector that takes a dynamic argument - reselect

I am trying to pass a dynamic argument to a reselect selector. The reason for this is that this argument is actually a angular route parameter that is not known in advance. It cannot be part of the state either.
Here is the relevant code from the subscribing component that passes the route parameter:
this.store.select(fromRoot.getMessagesWithOtherUserAccount(this.route.params['otherId']))
.subscribe(messages => this.messagesWithOtherUserAccount = messages);
Here is the code for the selectors:
const getMessagesState = (state: State) => state.message.messages;
//See error below... How can I pass my otherId argument here??
const messagesWithOtherUserAccount = createSelector(getMessagesState, messagesWithCounterParty);
export const getMessagesWithOtherUserAccount = (otherId: number) => messagesWithOtherUserAccount(otherId);
....
export const messagesWithCounterParty = (messages: Message[]) => (otherId: number) => withOtherUserAccount(otherId, messages);
Here is the error I get:
Argument of type 'number' is not assignable to parameter of type
'State'.
I would like to pass in the otherId argument to the messagesWithOtherUserAccount createSelector, but I am not sure how...
Can someone please help?

I was able to come up with the following solution:
this.store.select(fromRoot.getMessagesWithCounterParty(this.route.snapshot.params['otherId']))
.subscribe(messages => this.messagesWithOtherUserAccount = messages);
export const getMessagesWithCounterParty = (otherId: number) => createSelector(getMessagesState, (messages: Message[]) => withOtherUserAccount(otherId, messages));

createSelector can create selectors able to accept any number of custom/dynamic arguments! See createSelector API.
In your case the pseudo code to achive your result might be:
// ...
export const getMessagesWithCounterParty = createSelector(
getMessagesState, // Accepts the state as 1st argument
(otherId: number) => otherId, // Accepts an Id as 2nd argument
// Result function
(messages: Message[], otherId: number) => withOtherUserAccount(messages, otherId),
);
// Later in your application:
getMessagesWithCounterParty(yourState, 42);
PS.The error you get is not from your application but from your type checker (probably Typescript).

Related

createSelector is returning its equalityCheck function instead of the value

By the sake of understanding better createSelector, I'm trying to use it instead of useSelector
const domainEntitlements = useSelector((state) => state.objects.domainEntitlements[match.params.id]);
const domainEntitlement = createSelector((state) => state.objects.domainEntitlements, (domainEntitlements) => (domainEntitlements));
console.log(domainEntitlements, domainEntitlement);
the useSelector logs an object (correct), but the createSelector logs an equalityCheck function.
Am I doing something very stupid or why I can get the data with useSelector and not with createSelector?
CreateSelector returns a function, this function you can pass to useSelector as an argument.
CreateSelector is used to specify logic to where data is located in the state or how data is derived only once and re use them in other selectors. Here is how you can use your selector in the component:
const selectObjects = (state) => state.objects;
const selectDomainEntitlements = createSelector(
[selectObjects], //re use selectObjects
(objects) => objects.domainEntitlements
);
const createSelectDomainEntitlementById = (id) =>
createSelector(
[selectDomainEntitlements], //re use selecting domain entitlements
(domainEntitlements) => domainEntitlements[id]
);
//example of using the selector in your component
const Component = () => {
const domainEntitlement = useSelector(
createSelectDomainEntitlementById(match.params.id)
);
};
A more detailed explanation on selectors and how to possibly optimize your app by using memoization can be found here

Cypress get attribute value and assign it to variable in function

I am trying to get attribute value and return it from a function.
Here is the code that is working and can be used in a normal test class (into the integration folder).
describe('Example shows how to get attribute value.', () => {
// 'it' is used to create test case. You can add a name of the test case. You can have multiple test cases in one JS class.
it('Get attribute value.', () => {
// Cypress is not able to work with new tabs. It is not possible to switch between tabs. Cypress can manipulate the DOM tree, so we can change the element attributes and open the hyperlink in the same browser tab.
// 'visit()' method is used for navigating to URL address.
cy.visit('https://demoqa.com/links')
cy.xpath('//*[#id="simpleLink"]').then(function (element) {
// 'prop()' method is used to get the attribute value.
const url = element.prop('href')
cy.visit(url)
})
// Assert URL.
cy.url().should('include', 'demoqa.com').should('eq', 'https://demoqa.com/');
})
If I use the code that way - everything is working as expected.
But if I want to re-use the code and create a function like this:
// Give a value of the variable to use it for next function.
functionName = 'addAttribute';
// Declare a Cypress child custom command.
Cypress.Commands.add(functionName, { prevSubject: 'element' }, (subject: any, attributeName: string, attributeValue: string) => {
// Create a try-catch statement. If the function fails - we will recieve the error message.
try {
// Create the function steps after this comment.
cy
.wrap(subject)
.invoke('attr', attributeName, attributeValue)
.should('have.attr', attributeName, attributeValue)
} catch (error) {
// Create the error log and show it to the UI. Show the function name, the class where the function is located and catched error.
let errorMessage = `----------ERROR! It seems that we have an error. Please review the "${functionName}" function from "${__filename.split(__dirname + "/").pop()}" . The error is: ${error}`;
cy.log(errorMessage);
console.log(errorMessage);
}
})
The result is 'object' and I am not sure how to process it.
Here is the rest of the code:
describe("'getAttribute' custom child command example.", () => {
it("example shows how to use 'getAttribute' custom child command.", () => {
cy.visit('https://demoqa.com/buttons');
let attributeValue = cy.element('xpath','(//*[contains(text(),"Click Me")])[3]').getAttribute('class');
cy.log(`The attribute values is: ${attributeValue}`)
});
});
You have to do as below to have a return value:
let attributeValue = '';
cy.element('xpath', '(//*[contains(text(),"Click Me")])[3]')
.getAttribute('class')
.then((attr) => {
attributeValue = attr;
});
cy.log('The attribute values is:' + attributeValue)

rxjs why does behaviour subject when piped triggered with next()

I have read the tutorial from this link https://magarcia.io/2019/02/18/bloc-pattern-with-react-hooks
and i just dont understand how the search query to the API is triggered when _query.next is called with new search terms
see below code.
export class SearchBloc {
private _results$: Observable<string[]>;
private _query$ = new BehaviorSubject<string>('');
constructor(private api: API) {
**this._results$ = this._query$.pipe(
switchMap((query) => {
return observableFrom(this.api.search(query));
})
);**
get results$(): Observable<string[]> {
return this._results$;
}
}
const SearchInput = () => {
const searchBloc = useContext(SearchContext);
const [query, setQuery] = useState('');
useEffect(() => {
searchBloc.query.next(query);
}, [searchBloc, query]);
return (
<input
type="text"
name="Search"
value={query}
onChange={({ target }) => setQuery(target.value)}
/>
);
};
Assuming that searchblock was put in the context, and during input change the query which is a behaviour subject is assigned a new value with next();
how or why does the api query executes?
I guess I did not understand the line with
this._results$ = this._query$.pipe(
switchMap((query) => {
so maybe the question is, how did the pipe worked? did it create a method callback that will execute when next is called? and what is the assignment to result mean?
anyone that can help me make sense of it is greatly appreaciated.
Consider the following code:
It creates a stream of 5 numbers. Then it creates a second stream which is defined as a stream that has all the same numbers as the first one, only each number is incremented.
const numberStream$ = of(1,2,3,4,5);
const numbersPlus1$ = numberStream$.pipe(
map(v => v + 1)
);
numbersPlus1$.subscribe(console.log);
If you subscribe to numberStream$ you should expect to get 1,2,3,4,5.
If you subscribe to numbersPlus1$ you should expect to get 2,3,4,5,6.
Here we do the same thing with a Subject. Of course, unlike of(1,2,3,4,5), a subject lets you create a stream imperatively. Whenever I call .next on a subject, I'm saying "Make this value the next emission in this subject's stream."
const numberSubject$ = new Subject<number>();
const numbersPlus1$ = numberSubject$.pipe(
map(v => v + 1)
);
numbersPlus1$.subscribe(console.log);
numberSubject$.next(1);
numberSubject$.next(2);
numberSubject$.next(3);
numberSubject$.next(4);
numberSubject$.next(5);

How to override 'data-testid' in the 'findByTestId function from Cypress Testing Library

Most of my existing codebase uses a 'id' only in few places 'data-testId' attribute present.
tried this code
import { configure } from '#testing-library/cypress';
configure({ testIdAttribute: ['data-testId','id'] });
But, still its not working.
Is there any way to use 'id' value in any of the testing-library functions.
My HTML code is something like:
<div class="some random class name" id="userprofile-open" role="button">SB</div>
I want click that element with this code:
cy.findByTestId("userprofile-open", { timeout: 120000 }).click();
I don't think you can configure testing-library with an array of ids, ref API configuration,
import { configure } from '#testing-library/cypress'
configure({ testIdAttribute: 'id' })
But even this fails. Instead you have to use the Cypress command to change the attribute name (only one name is allowed).
cy.configureCypressTestingLibrary({ testIdAttribute: 'id' })
To use either/or attribute name you can change the attribute name on the fly, wrapping it in a custom command (based on Custom Queries)
Cypress.Commands.add('findByTestIdOrId', (idToFind) => {
let result;
const { queryHelpers } = require('#testing-library/dom');
let queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'data-testId');
result = queryAllByTestId(Cypress.$('body')[0], idToFind)
if (result.length) return result;
queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'id');
result = queryAllByTestId(Cypress.$('body')[0], idToFind);
if (result.length) return result;
throw `Unable to find an element by: [data-test-id="${idToFind}"] or [id="${idToFind}"]`
})
cy.findByTestIdOrId('my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
Note this custom command works only for synchronous DOM.
If you need to have Cypress retry and search for either/or attribute, don't use testing-library in the custom command.
Instead use Cypress .should() to enable retry
Cypress.Commands.add('findByTestIdOrId', (selector, idToFind) => {
cy.get(selector)
.should('satisfy', $els => {
const attrs = [...$els].reduce((acc, el) => {
const id = el.id || el.getAttribute('data-test-id') // either/or attribute
if (id) {
acc.push(id)
}
return acc
}, [])
return attrs.some(attr => attr === idToFind); // retries when false
})
.first(); // may be more than one
})
cy.findByTestIdOrId('div', 'my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
The usual cypress way - which has an inherent check on the element visibility and existence as well as included retries for a period of time is using cy.get()
If you want to select element using property like data-id you need this sintax: cy.get('[propertyName="propertyValue"]')
If you want select an element by CSS selector you just pass CSS selector like this:
cy.get('#id')

reselect CreateStructuredSelector difference in properties

Maybe this is not necessarily a reselect question
const makeSelectError = () => createSelector(
selectGlobal,
(globalState) => globalState.get('error')
);
and in reselect we use
const mapStateToProps = createStructuredSelector({
error: makeSelectError(),
});
why can't we use like below?
const makeSelectError = createSelector(
selectGlobal,
(globalState) => globalState.get('error')
);
and use like below in reselect
const mapStateToProps = createStructuredSelector({
error: makeSelectError,
});
Are there any issues/disadvantages with my code, or is that a standard practice?
Not only is the second way valid and correct it has other advantages.
With the first snippet you provide:
const makeSelectError = () => createSelector(
selectGlobal,
(globalState) => globalState.get('error')
);
makeSelectError is a factory function in that every time it is called it is returning a new and unique selector.
This means that every time a simple mapStateToProps function is called a new selector will be made and the result of the selector will be computed again.
This means that you will be losing the key benefit of reselect that is memoization.
So for simple cases you could just do the following:
const getSomePieceOfState = state => state.someArea.someDetail;
const getAnotherPieceOfState = state => state.anotherArea.anotherItem;
const getSomeCombinedState = createSelector(
getSomePieceOfState,
getAnotherPieceOfState,
(somePiece, anotherPiece) => somePiece + anotherPiece
);
const mapStateToProps = state => ({
someProp: getSomeCombinedState(state)
});
Note. it's common to prefix the name of selectors with get and to prefix the name of a selector factory (a function that returns a selector) with makeGet.
Some times creating a selector factory is necessary though if you wan't to make a selector that is dependant on a property that is not in the state.
You can read more about that here Accessing React Props in Selectors
You're doing it absolutely right in the second example. This is a standard practice.
There's no need to do wrap makeSelectError to another function.

Resources