I have started learning state management using NGXS. So far everything is fine but have few questions regarding some scenarios like -
If a Mat Dialog box is open (or any div - here I've both the scenarios in my project) and from inside it an API is called, how can I close the dialog only if API returns success?
Suppose a user logs out, how can I reset the states to default values?
For the first case below is my code for the state, action & dispatcher:
abc.action.ts
export class AddExamCategory {
static readonly type = '[ExamCategory] Add';
constructor(public payload: ExamCategory) {}
}
abc.state.ts
export interface ExamCategoryStateModel {
examCategoryList: ExamCategory[];
}
#State<ExamCategoryStateModel>({
name: 'examCategory',
defaults: {
examCategoryList: []
}
})
#Injectable()
export class ExamCategoryState {
constructor(private _adminService: AdminService) {}
#Action(AddExamCategory)
addExamCategory({ getState, patchState }: StateContext<ExamCategoryStateModel>, { payload }: AddExamCategory) {
return this._adminService.createExamCategory(payload).pipe(tap(response => {
patchState({ examCategoryList: [] }); // Want to close the modal/div after this part. If API returns ERROR do not close.
}));
}
}
abc.component.ts
this.store.dispatch(new AddAdminUser({ ...this.adminRegistrationForm.value, password: this.password.value }))
.subscribe(response => {
this.store.dispatch(new GetAdminUsers());
this.dialogRef.close(true)
});
Currently it's like this but it closes no matter what's the status of API.
For the second case, in the service where I have written the logic for logout() I have written like this: this.store.reset({}). Though it's resetting the state but not with the default values in it. I have multiple states to reset on this single logout method.
How to work on these scenarios?
You can add extra property on your state to track the requesting state of your application ('requesting','idle') [you can create extra states as needed to track 'success' and 'error' response from the server]
when dispatch GetAdminUsers set the value of the newely added state to requesting and at GetAdminUsersComplete set the value to idle
subscribe to a selector that's read the state on your ngOnInit and call dialogRef.clse(true) inside of it. like following:
this.store
.pipe(
select(selectors.selectRequestState),
skip(1) //only start tracking after request created
)
.subscribe(result => {
if (result == 'idle')
this.dialogRef.close()
});
example: https://stackblitz.com/edit/angular-ivy-4ofc3q?file=src/app/app.component.html
Reset State
I don't think there is a simple way to reset the state with the store. You need to move through all your state features and implement reset action that set the state to initial state.
the simplest solution is to refresh the browser page after user logout using location.reload();
if you keep the store inside localstorage you need to remove it first then do the reload
the easiest way to accomplish that is with an effect, after you dispatch your action you should be able to trigger other action like "UsserAddedSuccessfully" and read it after that close that modal.
read this question for more detail.
In my header component.ts
ngOnInit() {
this.authService.getUser((data)=>{
this.authService.userInfo.next(data)
})
}
In authService.ts
export class AuthService {
userInfo= new BehaviorSubject<any>(null);
}
In my another component I am subscribing the variable in ngOnInit
ngOnInit() {
this.auth.userInfo.subscribe((data)=>{
this.userInfo = data.user;
console.log('user data subsribed ',this.userInfo)
})
}
This works fine on route navigations but when I reload the page the subscriber is not triggering
There is a possibility that userInfo subject emitted before it be subscribed to. Which mean the subcribing component missed the data. I suggest to use ReplaySubject instead. It hold the data and late subcribing component will still get it.
Try to subscribe to the subject in the ngAfterViewInit life cycle hook instead of ngOnInit
You don't subscribe the this.authService.getUser() function. userInfo is subscribe but that one doesn't trigger the getUser() function.
From this example I don't know when it is working while navigation.
But aren't you making it too complex by introducing a behaviourSubject? Do you need this? If it's because you want to cache the result of the getUser call then you better add
getUser() => this.http.get<UserInfo>().pipe(share())
Or introduce a variable:
getUser() => this.userInfo ? of(this.userInfo) : this.http.get<UserInfo>().pipe(tap(userInfo => this.userInfo = userInfo))
I want to test the if the localStorage is cleared down whenever I call my function.
Component
ngOnInit() {
// Logout on reaching login screen so we can login
this.authService.logout();
}
authService
logout() {
// remove logged in user from localStorage
localStorage.removeItem('currentUser');
}
TEST
fit('ngOnInit should logout user stored in localStorage', () => {
// Exmample data stored as token
localStorage.setItem('token', 'someUserToken');
component.ngOnInit();
expect(localStorage.getItem('token')).toEqual('{}');
});
Is there any way I can achieve this?
Unit testing should only concern the feature you're testing.
This means that you should not check if the local storage is cleared : that's not what your component does. Your component calls your auth service, which then clears the storage.
This means you should test if the correct method is called.
it('ngOnInit should call auth.logout', () => {
spyOn(component['authService'], 'logout');
component.ngOnInit();
expect(component['authService'].logout).toHaveBeenCalled();
});
Reading through this article covering OnPush Change Detection and it features the following service that uses a BehaviorSubject to load a user:
const ANONYMOUS_USER: User = {
firstName: '',
lastName: ''
};
#Injectable()
export class UserService {
private subject = new BehaviorSubject<User>(ANONYMOUS_USER);
user$: Observable<User> = this.subject.asObservable();
loadUser(user:User) {
this.subject.next(user);
}
}
Suppose the subject reference is never updated with a new user. In other words it sticks with the ANONYMOUS_USER state that it is initialized with.
Will all components that are injected with UserService and call loadUser receive an instance of the ANONYMOUS_USER. IIUC that is the purpose of BehaviorSubject. It remembers the current state so that loadUser can be called multiple times by different components and they will always receive the ANONYMOUS_USER?
Not quite.
All components that subscribe to user$ will receive the ANONYMOUS_USER value from the subject.
So for example, if a component A subscribes, it will immediately get the ANONYMOUS_USER value. If the user navigates to another component B, the original component A is destroyed. If the user navigates back to component A and it resubscribes, it will get the ANONYMOUS_User value again.
This code:
loadUser(user:User) {
this.subject.next(user);
}
Is what broadcasts a new value into the observable stream. So if any components call this method, they will broadcast a new value ... whatever they passed in to the method.
I have a complete example of using BehaviorSubject here: https://github.com/DeborahK/MovieHunter-communication/tree/master/MH-Take3
And code that accomplishes the same task using getters/setters instead of BehaviorSubject here: https://github.com/DeborahK/MovieHunter-communication/tree/master/MH-Take4
EDIT:
The main purpose of a Subject is to broadcast events to subscribers. It allows components to call a method (loadUser in this example) and pass a value. That value is then broadcast to all subscribers.
If you are using data binding, in most cases a subject is not needed. That is because Angular's change detection will provide the notification automatically.
A BehaviorSubject is a special type of Subject that has an initial value and provides that initial value to the subscriber when they subscribe. It is often used instead of a Subject if you need the subject to "remember" its last value.
I'm trying to set up a React app where clicking a map marker in one component re-renders another component on the page with data from the database and changes the URL. It works, sort of, but not well.
I'm having trouble figuring out how getting the state from Redux and getting a response back from the API fit within the React life cycle.
There are two related problems:
FIRST: The commented-out line "//APIManager.get()......" doesn't work, but the hacked-together version on the line below it does.
SECOND: The line where I'm console.log()-ing the response logs infinitely and makes infinite GET requests to my database.
Here's my component below:
class Hike extends Component {
constructor() {
super()
this.state = {
currentHike: {
id: '',
name: '',
review: {},
}
}
}
componentDidUpdate() {
const params = this.props.params
const hack = "/api/hike/" + params
// APIManager.get('/api/hike/', params, (err, response) => { // doesn't work
APIManager.get(hack, null, (err, response) => { // works
if (err) {
console.error(err)
return
}
console.log(JSON.stringify(response.result)) // SECOND
this.setState({
currentHike: response.result
})
})
}
render() {
// Allow for fields to be blank
const name = (this.state.currentHike.name == null) ? null : this.state.currentHike.name
return (
<div>
<p>testing hike component</p>
<p>{this.state.currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
params: state.hike.selectedHike
}
}
export default connect(stateToProps)(Hike)
Also: When I click a link on the page to go to another url, I get the following error:
"Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op."
Looking at your code, I think I would architect it slightly differently
Few things:
Try to move the API calls and fetch data into a Redux action. Since API fetch is asynchronous, I think it is best to use Redux Thunk
example:
function fetchHikeById(hikeId) {
return dispatch => {
// optional: dispatch an action here to change redux state to loading
dispatch(action.loadingStarted())
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err);
// if you want user to know an error happened.
// you can optionally dispatch action to store
// the error in the redux state.
dispatch(action.fetchError(err));
return;
}
dispatch(action.currentHikeReceived(response.result))
});
}
}
You can map dispatch to props for fetchHikeById also, by treating fetchHikeById like any other action creator.
Since you have a path /hike/:hikeId I assume you are also updating the route. So if you want people to book mark and save and url .../hike/2 or go back to it. You can still put the the fetch in the Hike component.
The lifecycle method you put the fetchHikeById action is.
componentDidMount() {
// assume you are using react router to pass the hikeId
// from the url '/hike/:hikeId'
const hikeId = this.props.params.hikeId;
this.props.fetchHikeById(hikeId);
}
componentWillReceiveProps(nextProps) {
// so this is when the props changed.
// so if the hikeId change, you'd have to re-fetch.
if (this.props.params.hikeId !== nextProps.params.hikeId) {
this.props.fetchHikeById(nextProps.params.hikeId)
}
}
I don't see any Redux being used at all in your code. If you plan on using Redux, you should move all that API logic into an action creator and store the API responses in your Redux Store. I understand you're quickly prototyping now. :)
Your infinite loop is caused because you chose the wrong lifecycle method. If you use the componentDidUpdate and setState, it will again cause the componentDidUpdatemethod to be called and so on. You're basically updating whenever the component is updated, if that makes any sense. :D
You could always check, before sending the API call, if the new props.params you have are different than the ones you previously had (which caused the API call). You receive the old props and state as arguments to that function.
https://facebook.github.io/react/docs/react-component.html#componentdidupdate
However, if you've decided to use Redux, I would probably move that logic to an action creator, store that response in your Redux Store and simply use that data in your connect.
The FIRST problem I cannot help with, as I do not know what this APIManager's arguments should be.
The SECOND problem is a result of you doing API requests in "componentDidUpdate()". This is essentially what happens:
Some state changes in redux.
Hike receives new props (or its state changes).
Hike renders according to the new props.
Hike has now been updated and calls your "componentDidUpdate" function.
componentDidUpdate makes the API call, and when the response comes back, it triggers setState().
Inner state of Hike is changed, which triggers an update of the component(!) -> goto step 2.
When you click on a link to another page, the infinite loop is continued and after the last API call triggered by an update of Hike is resolved, you call "setState" again, which now tries to update the state of a no-longer-mounted component, hence the warning.
The docs explain this really well I find, I would give those a thorough read.
Try making the API call in componentDidMount:
componentDidMount() {
// make your API call and then call .setState
}
Do that instead of inside of componentDidUpdate.
There are many ways to architect your API calls inside of your React app. For example, take a look at this article: React AJAX Best Practices. In case the link is broken, it outlines a few ideas:
Root Component
This is the simplest approach so it's great for prototypes and small apps.
With this approach, you build a single root/parent component that issues all your AJAX requests. The root component stores the AJAX response data in it's state, and passes that state (or a portion of it) down to child components as props.
As this is outside the scope of the question, I'll leave you to to a bit of research, but some other methods for managing state and async API calls involved libraries like Redux which is one of the de-facto state managers for React right now.
By the way, your infinite calls come from the fact that when your component updates, it's making an API call and then calling setState which updates the component again, throwing you into an infinite loop.
Still figuring out the flow of Redux because it solved the problem when I moved the API request from the Hike component to the one it was listening to.
Now the Hike component is just listening and re-rendering once the database info catches up with the re-routing and re-rendering.
Hike.js
class Hike extends Component {
constructor() {
super()
this.state = {}
}
componentDidUpdate() {
console.log('dealing with ' + JSON.stringify(this.props.currentHike))
}
render() {
if (this.props.currentHike == null || undefined) { return false }
const currentHike = this.props.currentHike
return (
<div className="sidebar">
<p>{currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
currentHike: state.hike.currentHike,
}
}
And "this.props.currentHikeReceived()" got moved back to the action doing everything in the other component so I no longer have to worry about the Hikes component infinitely re-rendering itself.
Map.js
onMarkerClick(id) {
const hikeId = id
// Set params to be fetched
this.props.hikeSelected(hikeId)
// GET hike data from database
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err)
return
}
this.props.currentHikeReceived(response.result)
})
// Change path to clicked hike
const path = `/hike/${hikeId}`
browserHistory.push(path)
}
const stateToProps = (state) => {
return {
hikes: state.hike.list,
location: state.newHike
}
}
const dispatchToProps = (dispatch) => {
return {
currentHikeReceived: (hike) => dispatch(actions.currentHikeReceived(hike)),
hikesReceived: (hikes) => dispatch(actions.hikesReceived(hikes)),
hikeSelected: (hike) => dispatch(actions.hikeSelected(hike)),
locationAdded: (location) => dispatch(actions.locationAdded(location)),
}
}