I’m currently evaluating the pros ‘n’ cons of replacing Angular’s resp. RxJS’ Observable with plain Promise so that I can use async and await and get a more intuitive code style.
One of our typical scenarios: Load some data within ngOnInit. Using Observables, we do:
ngOnInit () {
this.service.getData().subscribe(data => {
this.data = this.modifyMyData(data);
});
}
When I return a Promise from getData() instead, and use async and await, it becomes:
async ngOnInit () {
const data = await this.service.getData();
this.data = this.modifyMyData(data);
}
Now, obviously, Angular will not “know”, that ngOnInit has become async. I feel that this is not a problem: My app still works as before. But when I look at the OnInit interface, the function is obviously not declared in such a way which would suggest that it can be declared async:
ngOnInit(): void;
So -- bottom line: Is it reasonable what I’m doing here? Or will I run into any unforseen problems?
It is no different than what you had before. ngOnInit will return a Promise and the caller will ignore that promise. This means that the caller will not wait for everything in your method to finish before it proceeds. In this specific case it means the view will finish being configured and the view may be launched before this.data is set.
That is the same situation you had before. The caller would not wait for your subscriptions to finish and would possibly launch the app before this.data had been populated. If your view is relying on data then you likely have some kind of ngIf setup to prevent you from accessing it.
I personally don't see it as awkward or a bad practice as long as you're aware of the implications. However, the ngIf can be tedious (they would be needed in either way). I have personally moved to using route resolvers where it makes sense so I can avoid this situation. The data is loaded before the route finishes navigating and I can know the data is available before the view is ever loaded.
Now, obviously, Angular will not “know”, that ngOnInit has become async. I feel that this is not a problem: My app still works as before.
Semantically it will compile fine and run as expected, but the convenience of writing async / wait comes at a cost of error handling, and I think it should be avoid.
Let's look at what happens.
What happens when a promise is rejected:
public ngOnInit() {
const p = new Promise((resolver, reject) => reject(-1));
}
The above generates the following stack trace:
core.js:6014 ERROR Error: Uncaught (in promise): -1
at resolvePromise (zone-evergreen.js:797) [angular]
at :4200/polyfills.js:3942:17 [angular]
at new ZoneAwarePromise (zone-evergreen.js:876) [angular]
at ExampleComponent.ngOnInit (example.component.ts:44) [angular]
.....
We can clearly see that the unhandled error was triggered by a ngOnInit and also see which source code file to find the offending line of code.
What happens when we use async/wait that is reject:
public async ngOnInit() {
const p = await new Promise((resolver, reject) => reject());
}
The above generates the following stack trace:
core.js:6014 ERROR Error: Uncaught (in promise):
at resolvePromise (zone-evergreen.js:797) [angular]
at :4200/polyfills.js:3942:17 [angular]
at rejected (tslib.es6.js:71) [angular]
at Object.onInvoke (core.js:39699) [angular]
at :4200/polyfills.js:4090:36 [angular]
at Object.onInvokeTask (core.js:39680) [angular]
at drainMicroTaskQueue (zone-evergreen.js:559) [<root>]
What happened? We have no clue, because the stack trace is outside of the component.
Still, you might be tempted to use promises and just avoid using async / wait. So let's see what happens if a promise is rejected after a setTimeout().
public ngOnInit() {
const p = new Promise((resolver, reject) => {
setTimeout(() => reject(), 1000);
});
}
We will get the following stack trace:
core.js:6014 ERROR Error: Uncaught (in promise): [object Undefined]
at resolvePromise (zone-evergreen.js:797) [angular]
at :4200/polyfills.js:3942:17 [angular]
at :4200/app-module.js:21450:30 [angular]
at Object.onInvokeTask (core.js:39680) [angular]
at timer (zone-evergreen.js:2650) [<root>]
Again, we've lost context here and don't know where to go to fix the bug.
Observables suffer from the same side effects of error handling, but generally the error messages are of better quality. If someone uses throwError(new Error()) the Error object will contain a stack trace, and if you're using the HttpModule the Error object is usually a Http response object that tells you about the request.
So the moral of the story here: Catch your errors, use observables when you can and don't use async ngOnInit(), because it will come back to haunt you as a difficult bug to find and fix.
I wonder what are the downsides of an immediately invoked function expression :
ngOnInit () {
(async () => {
const data = await this.service.getData();
this.data = this.modifyMyData(data);
})();
}
It is the only way I can imagine to make it work without declaring ngOnInit() as an async function
I used try catch inside the ngOnInit():
async ngOnInit() {
try {
const user = await userService.getUser();
} catch (error) {
console.error(error);
}
}
Then you get a more descriptive error and you can find where the bug is
ngOnInit does NOT wait for the promise to complete. You can make it an async function if you feel like using await like so:
import { take } from 'rxjs/operators';
async ngOnInit(): Promise<any> {
const data = await firstValueFrom(this.service.getData());
this.data = this.modifyMyData(data);
}
However, if you're using ngOnInit instead of the constructor to wait for a function to complete, you're basically doing the equivalent of this:
import { take } from 'rxjs/operators';
constructor() {
firstValueFrom(this.service.getData())
.then((data => {;
this.data = this.modifyMyData(data);
});
}
It will run the async function, but it WILL NOT wait for it to complete. If you notice sometimes it completes and sometimes it doesn't, it really just depends on the timing of your function.
Using the ideas from this post, you can basically run outside zone.js. NgZone does not include scheduleMacroTask, but zone.js is imported already into angular.
Solution
import { isObservable, Observable } from 'rxjs';
import { take } from 'rxjs/operators';
declare const Zone: any;
async waitFor<T>(prom: Promise<T> | Observable<T>): Promise<T> {
if (isObservable(prom)) {
prom = firstValueFrom(prom);
}
const macroTask = Zone.current
.scheduleMacroTask(
`WAITFOR-${Math.random()}`,
() => { },
{},
() => { }
);
return prom.then((p: T) => {
macroTask.invoke();
return p;
});
}
I personally put this function in my core.module.ts, although you can put it anywhere.
Use it like so:
constructor(private cm: CoreModule) {
const p = this.service.getData();
this.post = this.cm.waitFor(p);
}
You could also check for isBrowser to keep your observable, or wait for results.
Conversely, you could also import angular-zen and use it like in this post, although you will be importing more than you need.
J
UPDATE: 2/26/22 - Now uses firstValueFrom since .toPromise() is depreciated.
You can use rxjs function of.
of(this.service.getData());
Converts the promise to an observable sequence.
The correct answer to this question is to use Resolver feature of Angular. With Resolve, your component will not be rendered until you tell it to render.
From docs:
Interface that classes can implement to be a data provider. A data
provider class can be used with the router to resolve data during
navigation. The interface defines a resolve() method that is invoked
when the navigation starts. The router waits for the data to be
resolved before the route is finally activated.
I would do this a bit differently, you don't show your html template, but I'm assuming you're doing something in there with data, i.e.
<p> {{ data.Name }}</p> <!-- or whatever -->
Is there a reason you're not using the async pipe?, i.e.
<p> {{ (data$ | async).Name }}</p>
or
<p *ngIf="(data$ | async) as data"> {{ data.name }} </p>
and in your ngOnInit:
data$: Observable<any>; //change to your data structure
ngOnInit () {
this.data$ = this.service.getData().pipe(
map(data => this.modifyMyData(data))
);
}
Related
I am trying to run a method from my smart contract located on the ropsten test network but am running into the following error in my getBalance() function:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'methods')
46 | async function getBalance() {
> 47 | const tempBal = await window.contract.methods.balanceOf(account.data).call()
| ^
48 | setBalance(tempBal)
49 | }
Connecting my contract:
export default function ConnectContract() {
async function loadWeb3() {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
window.ethereum.enable()
}
}
async function loadContract() {
const tempContract = await new window.web3.eth.Contract(ABI, ContractAddress)
return tempContract
}
async function load() {
await loadWeb3();
window.contract = await loadContract();
}
load();
}
Currently the functions are being called like so:
export default function Page() {
ConnectContract();
getBalance();
}
Another way I previous called it that did not result in the undefined error was by using an onClick function inside a button to getBalance:
<div><button onClick={() => getBalance}>Get Balance</button></div>
I know calling them like this is causing the getBalance function method to be called before the contract is connected to the website but with the current code, it works after 1-page refresh. But I have not been able to find a solution to make the contract be loaded so the method is defined by the time getBalance is called.
I have tried doing onLoad function and window.load() functions with no success. I have also tried calling my ConnectContract function in the _app.js to load it when the website is launched but that had no success as well.
You have to call the loadWeb3 function inside useEffect hook. useEffect is used to prepare the state for the component when it is mounted. to keep track of the state, use useState hook
const [web3,setWeb3]=useState('')
const [contract,setContract]=useState('')
async function loadWeb3() {
if (window.ethereum) {
window.ethereum.enable()
setWeb3(new Web3(window.ethereum))
}
}
// [] array means we want this useEffect code run only when the compoenent rerenders
// Since loadWeb3 is async function, call it inside try/catch block
useEffect(()=>{loadWeb3()},[])
Next we you need to prepare contract same way.
async function loadContract() {
const tempContract = await new web3.eth.Contract(ABI, ContractAddress)
setContract(tempContract)
}
You also need to call this in useEffect but this time its dependencies are different. Because in order to get the contract, we are relying on web3
// Since loadContract is async function, call it inside try/catch block
useEffect(()=>{loadContract},[web3])
when component renders, first useEffect will run and set the web3. Since web3 is changed, component will rerender again this useEffect will run. So when your component is mounted, you will have web3 and contract set so you can use those.
Your issue is in this code right here:
46 | async function getBalance() {
> 47 | const tempBal = await window.contract.methods.balanceOf(account.data).call()
| ^
48 | setBalance(tempBal)
49 | }
My guess is that the window.contract either doesn't exist or isn't getting loaded before this gets called. Perhaps it is either getting called in a serverside place where the window doesn't exist or try wrapping it in an if function so that it checks to make sure it exists before calling it
I am using Axios to do requests to my server. I am aware of how to mock Axios and supply positive and negative results to my code under test. But today I ran into a case where my code calls two api end points and to test the logic properly, I need to be able to control the order that they are resolved in.
Without doing anything special, I jest will resolve the first call first and then resolve the second call. But I need to ensure that my code can handle the calls being resolved in the opposite order. I can't figure out how to get jest to resolve the second call before resolving the first call.
A simple example of the code under test would be:
function underTest() {
axios.request('get', url1).then(() => { do something });
axios.request('get', url2).then(() => { do something else });
}
I have to ensure that the code does the right thing when having "do something" and "do something else" run in both sequences.
#Bergi asked how I am mocking the Axios requests now, so here goes.
In my production code I use NTypewriter to create Typescript classes for each C# controller I have on the back end. It produces something like this:
export class LeagueService {
public static getManagementSummaryRoute = (leagueId: number) => `/api/League/${leagueId}/ManagementSummary`;
public static getManagementSummary(leagueId: number) : Promise<LeagueManagementSummary> {
return MakeRequest<LeagueManagementSummary>("get", this.getManagementSummaryRoute(leagueId), null);
}
where MakeRequest<> is:
import axios, { AxiosResponse, Method } from 'axios';
export async function MakeRequest<T>(httpMethod: Method, url: string,
payload: any | undefined = null): Promise<T>
{
const r = await axios.request({
method: httpMethod,
url,
data: payload
});
return r.data;
}
This lets me call out to the end point like this:
BoxLeagueService.getSession(this.leagueId, this.sessionId)
.then((newData: SessionBox) => {
box.updateData(newData);
})
.catch();
Ok, then in the unit test code, I have to import axios and mock it. Jest has great support for axios.
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
then in a test, I do something like this:
it('constructor should request the league info with live layout When preview is false', () => {
// Arrange
mockedAxios.request.mockResolvedValueOnce({ data: leagueInfo });
mockedAxios.request.mockClear();
// Act
model = new BLInfoModel(leagueIdStr, false);
// Assert
expect(mockedAxios.request).toHaveBeenCalledWith({
method: 'get',
url: BoxLeagueService.infoPageRoute(leagueId, false),
data: null
});
});
the data: leagueInfo is the data that I am supplying from the server. I store these in .json files in a folder called json so that I can find them easily and add to them easily.
I just import them into my unit test at the top of the file like this:
// Test Data
import leagueInfo from './__json__/LeagueInfo.json';
This all works great when I am happy having jest fulfill the axios requests in the order that they are called. But today, after many years of doing this, I ran across a situation where I wanted to test that resolving two requests in the opposite order still produced the proper result.
in the then callbacks, you could define some timeouts, setting different timeout values depending on which should resolve first/last
const delay = (ms) => () => new Promise((res) => setTimeout(res, ms))
axios.request('get', url1)
.then(delay(10))
.then(() => { do something }); // resolves first
axios.request('get', url2)
.then(delay(20))
.then(() => { do something else }); // resolves last
edit...
I just realised this is probably your production code, not test code. Either way you could apply the same thing to axios mock, i.e. apply a delay to a chained then callback
I have the configuration of a service inside a component in React and I am having problems with jest and testing-library, the app is working but the test is blocking.
import { appSetupConfig } from '.../myapp'
import theConfig from '.../config'
useEffect(() => {
const allowAppInstance = appSetupConfig();
allowAppInstance.get(theConfig).then((value) => {
if (value.something) {
Do Something;
}
...the rest of code
}, []);
This theConfig is an external file containing an object.
This is the error:
TypeError: Cannot read property 'get' of undefined
37 | const allowAppInstance = appSetupConfig();
38 |
> 39 | allowAppInstance.get(theConfig).then((value) => {
Is there any way to mock this get in jest's setup.js?
I don’t necessarily need to test this item yet, but I can’t proceed without it.
Yes, there is. So it would seem that you have called jest.mock('.../myapp') or similar at some point. In the mock object that Jest creates for the module, every mock function returns undefined. You need to mock a return value on appSetupConfig that is itself a mock object with the method(s) you need like get. Then get in turn needs to return a mock promise, and so on as deeply as needed. In your setup file, this would look like:
import { appSetupConfig } from '.../myapp'
...
jest.mock('.../myapp');
appSetupConfig.mockReturnValue({
get: jest.fn().mockResolvedValue({ something: jest.fn() }),
});
Your .then block will then be called in the test(s) with value set to undefined, but you can mock a different resolved value or a rejection of the promise for particular tests.
I have a function as follows:
recieveUserData = async() =>{
console.log(firebase.auth().currentUser.uid);
let user =firebase.auth().currentUser.uid;
await firebase.database().ref('/Users/'+user+'/Public').once('value').catch(error=>{console.log("error:" + error)}).then((snapshot)=>{
//Do something.
})
}
The console log, actually produces the uid. However, the function that requires the variable 'user' and hence 'firebase.auth().currentUser.uid' fails:
[Unhandled promise rejection: TypeError: Cannot read property 'uid' of null].
I am thinking that perhaps firebase.auth() may take some time to be called and hence the later line errors out. I thought I could solve this by changing the above to something like the following:
console.log(firebase.auth().currentUser.uid);
await firebase.auth()
.catch(err=>{console.log(err)})
.then(response=>{console.log(response)});
I was thinking I could write my later lines in the .then(), but I get a different error:
Unhandled promise rejection: TypeError: _firebase.default.auth(...).catch is not a function]
I am very confused as I use something similar in a different module and it seems to work fine. I am slightly worried that it will break now given these errors. Any insight would be really handy.
Thanks in advance
You do not need to use both sync and asynchronism at the same time.
You are not familiar with the use of 'async' and 'await'. If you understand 'sync', you can see the problem with your code. You made it a 'async' function and put 'await' in the firebase. Then the function starts from firebase, so the value of 'user' is not defined, and it generates null. This results in an error. Promise of '.then' is same the 'async' and 'await' function.
Could you try this code?
//async
const {currentUser} = await firebase.auth()
let userUid = currentUser.uid
//sync
let userUid;
firebase.auth().then( currentUser => {
userUid = currentUser.uid
})
.catch(error => console.log(error) )
In the end I managed to test whether I was authenticated in ComponentDidMount in a parent module and render the relevent modules only if I was authenticated. I also used firebase.auth().onAuthStateChanged as this would update as soon as I had been authenticated, i.e. it gave firebase enough time to return an actual currentUser and not null. This then meant my initial code/all other different solutions I have tried now actually work:
class WholeProject extends Component {
state={
user:null
}
componentDidMount = () => {
firebase.auth().onAuthStateChanged((user)=>{
if (user) {
this.setState({ user });
} else {
this.setState({ user: null });
}
});}
render(){
let authenticated;
if (this.state.user === null)
authenticated=<Text>Unauthenticated</Text>;
else authenticated=<Profile user = {this.state.user}/>;
return(
<React.Fragment>
{authenticated}
</React.Fragment>
);
}
}
export default WholeProject;
I have the following (simplified) React component.
class SalesView extends Component<{}, State> {
state: State = {
salesData: null
};
componentDidMount() {
this.fetchSalesData();
}
render() {
if (this.state.salesData) {
return <SalesChart salesData={this.state.salesData} />;
} else {
return <p>Loading</p>;
}
}
async fetchSalesData() {
let data = await new SalesService().fetchSalesData();
this.setState({ salesData: data });
}
}
When mounting, I fetch data from an API, which I have abstracted away in a class called SalesService. This class I want to mock, and for the method fetchSalesData I want to specify the return data (in a promise).
This is more or less how I want my test case to look like:
predefine test data
import SalesView
mock SalesService
setup mockSalesService to return a promise that returns the predefined test data when resolved
create the component
await
check snapshot
Testing the looks of SalesChart is not part of this question, I hope to solve that using Enzyme. I have been trying dozens of things to mock this asynchronous call, but I cannot seem to get this mocked properly. I have found the following examples of Jest mocking online, but they do not seem to cover this basic usage.
Hackernoon: Does not use asychronous calls
Wehkamp tech blog: Does not use asynchronous calls
Agatha Krzywda: Does not use asynchronous calls
GitConnected: Does not use a class with a function to mock
Jest tutorial An Async Example: Does not use a class with a function to mock
Jest tutorial Testing Asynchronous Code: Does not use a class with a function to mock
SO question 43749845: I can't connect the mock to the real implementation in this way
42638889: Is using dependency injection, I am not
46718663: Is not showing how the actual mock Class is implemented
My questions are:
How should the mock class look like?
Where should I place this mock class?
How should I import this mock class?
How do I tell that this mock class replaces the real class?
How do set up the mock implementation of a specific function of the mock class?
How do I wait in the test case for the promise to be resolved?
One example that I have that does not work is given below. The test runner crashes with the error throw err; and the last line in the stack trace is at process._tickCallback (internal/process/next_tick.js:188:7)
# __tests__/SalesView-test.js
import React from 'react';
import SalesView from '../SalesView';
jest.mock('../SalesService');
const salesServiceMock = require('../SalesService').default;
const weekTestData = [];
test('SalesView shows chart after SalesService returns data', async () => {
salesServiceMock.fetchSalesData.mockImplementation(() => {
console.log('Mock is called');
return new Promise((resolve) => {
process.nextTick(() => resolve(weekTestData));
});
});
const wrapper = await shallow(<SalesView/>);
expect(wrapper).toMatchSnapshot();
});
Sometimes, when a test is hard to write, it is trying to tell us that we have a design problem.
I think a small refactor could make things a lot easier - make SalesService a collaborator instead of an internal.
By that I mean, instead of calling new SalesService() inside your component, accept the sales service as a prop by the calling code. If you do that, then the calling code can also be your test, in which case all you need to do is mock the SalesService itself, and return whatever you want (using sinon or any other mocking library, or even just creating a hand rolled stub).
You could potentially abstract the new keyword away using a SalesService.create() method, then use jest.spyOn(object, methodName) to mock the implementation.
import SalesService from '../SalesService ';
test('SalesView shows chart after SalesService returns data', async () => {
const mockSalesService = {
fetchSalesData: jest.fn(() => {
return new Promise((resolve) => {
process.nextTick(() => resolve(weekTestData));
});
})
};
const spy = jest.spyOn(SalesService, 'create').mockImplementation(() => mockSalesService);
const wrapper = await shallow(<SalesView />);
expect(wrapper).toMatchSnapshot();
expect(spy).toHaveBeenCalled();
expect(mockSalesService.fetchSalesData).toHaveBeenCalled();
spy.mockReset();
spy.mockRestore();
});
One "ugly" way I've used in the past is to do a sort of poor-man's dependency injection.
It's based on the fact that you might not really want to go about instantiating SalesService every time you need it, but rather you want to hold a single instance per application, which everybody uses. In my case, SalesService required some initial configuration which I didn't want to repeat every time.[1]
So what I did was have a services.ts file which looks like this:
/// In services.ts
let salesService: SalesService|null = null;
export function setSalesService(s: SalesService) {
salesService = s;
}
export function getSalesService() {
if(salesService == null) throw new Error('Bad stuff');
return salesService;
}
Then, in my application's index.tsx or some similar place I'd have:
/// In index.tsx
// initialize stuff
const salesService = new SalesService(/* initialization parameters */)
services.setSalesService(salesService);
// other initialization, including calls to React.render etc.
In the components you can then just use getSalesService to get a reference to the one SalesService instance per application.
When it comes time to test, you just need to do some setup in your mocha (or whatever) before or beforeEach handlers to call setSalesService with a mock object.
Now, ideally, you'd want to pass in SalesService as a prop to your component, because it is an input to it, and by using getSalesService you're hiding this dependency and possibly causing you grief down the road. But if you need it in a very nested component, or if you're using a router or somesuch, it's becomes quite unwieldy to pass it as a prop.
You might also get away with using something like context, to keep everything inside React as it were.
The "ideal" solution for this would be something like dependency injection, but that's not an option with React AFAIK.
[1] It can also help in providing a single point for serializing remote-service calls, which might be needed at some point.