I have an application that has state classes for a number of topic areas in the application. Say it is a chat app and the the topics are users, chat messages and chat rooms. The user is authenticated/authorized by logging in. From there after the state is depends on the user that is logged in. When the user logs out, the app needs to reset the state of all of the 'topics' to their default state.
Questions:
What's the best way to organize these states? It seems like a good usage of substates, but the substate documentation talks about how to setup substates but doesn't show any examples of what it means for the states to be 'bound together'
How do I reset all of the states? Is this a good usage of the reset API?
Each feature module should define its own slice of state in a pair of files named feature-name.actions.ts and feature-name.state.ts, located inside the feature subdirectory (see the official style guide).
As you said, each feature state can respond to actions defined in other states, and update its own state accordingly. Here's an example:
src/app/auth/auth.state.ts:
...
// Import our own actions, including the Logout action
import { Logout, ... } from './auth.actions';
export interface AuthStateModel {
token?: string;
currentUser?: User;
permissions: string[];
}
const defaults: AuthStateModel = {
token : null,
currentUser: null,
permissions: [],
};
#State<AuthStateModel>({
name: 'auth',
defaults
})
export class AuthState {
...
// Respond to the Logout action from our own state
#Action(Logout)
logout(context: StateContext<AuthStateModel>) {
context.setState({ ...defaults });
}
...
}
src/app/users/users.state.ts:
...
// Import our own actions
import { LoadAllUsers, ... } from './users.actions';
// Import the Logout action from the Auth module
import { Logout } from '../auth/auth.actions';
export interface UsersStateModel {
users?: User[];
}
const defaults: UsersStateModel = {
users: null,
};
#State<UsersStateModel>({
name: 'users',
defaults
})
export class UsersState {
...
// An example of the usual case, responding to an action defined in
// our own feature state
#Action(LoadAllUsers)
loadUsers(context: StateContext<UsersStateModel>, action: LoadAllUsers) {
...
}
// Respond to the Logout action from the Auth state and reset our state (note
// that our context is still of type StateContext<UsersStateModel>, like the other
// actions in this file
#Action(Logout)
logout(context: StateContext<UsersStateModel>) {
context.setState({ ...defaults });
}
...
}
Note that although AuthState.logout() and UsersState.logout() both respond to the Logout action (defined in the AuthState module), the AuthState.logout() function accepts a context of type StateContext<AuthStateModel>, because we want to call that context's setState() function to update the 'auth' feature state. However, the UsersState.logout() function accepts a context of type StateContext<UsersStateModel>, because we want to call that context's setState() function to reset the 'users' feature state.
Each additional feature module can respond to the Logout action in the same way as UsersState did, and reset their own slice of the state.
After some additional research and experimentation, I can answer the 2nd question - 'how do I reset all of the states?' I was thinking of the action classes as being exclusively associated with the state they manage - they are not. States can handle any action you choose. So:
The header component injects the Store service.
The header's onLogout dispatches a Logout action.
The auth state response by resetting a stored JWT
Any other state can respond to Logout to reset itself
Related
Is it possible to call a cloud function that returns objects without having a current user? The iOS and Android SDKs support anonymous users but I'm asking specifically for JavaScript.
I'd like to allow it so that anyone who visits my web app can read objects without having to sign in. I'm using Back4App.
Yes. You can call a cloud code function no matter the user is logged in or not. Inside the cloud function you can check the user property of the request object to check if the user is either logged in or not. In the case that your user is not logged in and you want to query a class which requires user permission, you can use the useMasterKey option.
Parse.Cloud.define('myFunction', async req => {
const { user, isMaster } = req;
if (isMater) {
// the cloud code function was called using master key
} else if (user) {
// the cloud code function was called by an authenticated user
} else {
// the cloud code function was called without passing master key nor session token - not authenticated user
}
const obj = new Parse.Object('MyClass');
await obj.save(null, { useMasterKey: true }); // No matter if the user is authenticated or not, it bypasses all required permissions - you need to know what you are doing since anyone can fire this function
const query = new Parse.Query('MyClass');
return query.find({ useMasterKey: true }) // No matter if the user is authenticated or not, it bypasses all required permissions - you need to know what you are doing since anyone can fire this function
});
The Problem
When navigating away from query components that use the state of the app route as required variables, I get GraphQL errors of the sort:
Variable "$analysisId" of required type "ID!" was not provided.
"Navigating away" means, for example, going
from: /analysis/analysis-1/analyse/
to: /user-profile/
Background
I am building an SPA using Apollo GraphQL, and I have some queries which follow this pattern:
query Analyse($analysisId: ID!) {
location #client {
params {
analysisId #export(as: "analysisId")
}
}
analysis(analysisId: $analysisId) {
id
# ... etc
}
}
The location field gets a representation of the SPA router's state. That state is held in an Apollo client "reactive variable". Query components are programmed to not begin subscribing to the query unless that reactive variable exists and has the required content.
shouldSubscribe(): boolean {
return !!(locationVar()?.params?.analysisId);
}
Params represents express-style URL params, so the route path is /analysis/:analysisId/analyse.
If the user navigates to /analysis/analysis-1/analyse, the query component's variables become: { analysisId: "analysis-1" }`. This works fine when loading the component.
What I Think is Happening
When the component connects to the DOM, it checks to see if it's required variables are present in the router state, and if they are, it creates an ObservableQuery and subscribes.
Later, when the user navigates away, the ObservableQuery is still subscribed to updates when suddenly the required analysisId variable, exported by the client field location.params.analysisId is nullified.
I think that since the ObservableQuery is still subscribed, it sends off the query with null analysisId variable, even though it's required.
What I've Tried
By breaking on every method in my query component base class, I'm reasonably sure that the component base class is not at fault - there's no evidence that it is refetching the component when the route changes. Instead, I think this is happening inside the apollo client.
I could perhaps change the schema for the query from analysis(analysisId: ID!): Analysis to analysis(analysisId: ID): Analysis, but that seems roundabout, as I might not have control over the server.
How do I prevent apollo client from trying to fetch a query when it has required variables and they are not present?
This seems to be working fine so far, in my HttpLink, src/apollo/link/http.ts:
import { ApolloLink, from } from '#apollo/client/link/core';
import { HttpLink } from '#apollo/client/link/http';
import { hasAllVariables } from '#apollo-elements/lib/has-all-variables';
const uri =
'GRAPHQL_HOST/graphql';
export const httpLink = from([
new ApolloLink((operation, forward) => {
if (!hasAllVariables(operation))
return;
else
return forward(operation);
}),
new HttpLink({ uri }),
]);
I have a React-Redux app which displays a dashboard page when the user first logs on. The dashboard displays a number of statistics/graphs etc. which are components most (not all) of which are filtered based on a state site value. This site value is initially set when the user logs on and is derived from their profile.
The components mostly follow the pattern of using the componentDidMount lifecycle event to call a thunk method. Within the thunk method the site value is retrieved from redux state and passed in the database query. Reducer then adds results to state. Standard stuff. The redux state is fairly flat i.e. the state for the statistics/graphs etc. are not nested under the selected site but are their own objects off the root.
On the dashboard is also a dropdownlist which contains all sites. Initially this is set to the users default site. This dropdown is intended to allow the user to see statistics/graphs for sites other than their default.
What I would like is for all the (site specific) dashboard components to refresh when the user selects a different site from the dropdownlist. The problem I am having is how do I get these components to refresh? or more specifically, how to get their state to refresh.
UPDATE:
I tried two different approaches.
In the thunk action for handling the site change effect (changeSite) I added dispatch calls to each components thunk action. Although this worked I didn't like the fact that the changeSite thunk needed to know about the other components and what action creators to call. i.e. thunk action looked like:
changeSite: (siteId: number): AppThunkAction<any> => async (dispatch) => {
const promiseSiteUpdate = dispatch({ type: 'CHANGE_SITE', siteId });
await Promise.all([promiseSiteUpdate]);
dispatch(AncillariesStore.actionCreators.requestExpiring(siteId));
dispatch(AncillariesStore.actionCreators.requestExpired(siteId));
dispatch(FaultsStore.actionCreators.requestFaults(siteId));
dispatch(AssetsStore.actionCreators.requestAssetsCount(siteId));
dispatch(LicencesStore.actionCreators.requestLicencesCount(siteId));
dispatch(MaintenancesStore.actionCreators.requestMaintenancesCount(siteId));
dispatch(FaultsStore.actionCreators.requestFaultsCount(siteId));
}
Within a dependant component, included the Site value in the component properties, hooked into the componentDidUpdate lifecycle event. Checked if the Site had changed and then called the thunk action for updating the component state. I preferred this approach as it kept the business logic within the component so the changeSite thunk could now become a simple reducer call. An example of a dependent (site faults) component looks like this:
type FaultsProps =
{
faults: FaultsStore.FaultsItem[],
currentSiteId: number
}
& typeof FaultsStore.actionCreators;
const mapStateToProps = (state: ApplicationState) => {
return {
faults: state.faults?.faults,
currentSiteId: state.sites?.currentSiteId
}
}
class FaultyListContainer extends React.PureComponent<FaultsProps> {
public componentDidMount() {
// First load.
this.props.requestFaults(this.props.currentSiteId);
}
public componentDidUpdate(prevProps: FaultsProps) {
// Update state if site has changed.
if(prevProps.currentSiteId !== this.props.currentSiteId) {
this.props.requestFaults(this.props.currentSiteId);
}
}
public render() {
return React.createElement(FaultsList, {faults: this.props.faults});
}
}
export default connect(
mapStateToProps,
FaultsStore.actionCreators
)(FaultyListContainer as any);
Is #2 the best approach? is there a better way?
I am using firebase with authentication and firestore. Authenticated users are stored in database upon signup with extra roles field where some user have 'admin' role. I want to protect the admin route and i use the canActivate route guard. In the canActivate function I use 2 observables. The first 1 gets the logged in user and nested in that another observable which gets the saved users from firestore, uses the imformation from there o cehck if the logged in user is admin or not. The problem is that the routeguard works fine when the browser refreshes after the route types in the broser but when the route is called with the routerlink on the button, nothing happens.
tried using sycnronous values but those work only once and stop working after navigating around.
The canActivate function:
https://i.imgur.com/Rf0BZ79.png
canActivate(router: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.afAuth.getStateOnly().pipe(switchMap(user => { return this.db.getRole(user.uid) }))
}
The observables used:
https://i.imgur.com/hRp8zyf.png
https://i.imgur.com/oV56AGl.png
// user object or null from firebase
getStateOnly() {
return this.afAuth.authState
}
//checking the role and returning true or false(userArr is an array of users observable to store the users from firestore)
getRole(uid: string): Observable<boolean> {
return this.userArr.pipe(map(users => {
return users.filter(user => {
if (user.uid === uid) {
return user
}
})[0].roles.includes('admin')
}))
So this works only when i type the path in the browser and the browser refreshes when loading the route. It is not working when i click the admin button to navigate to admin page (not with routerLink nor router.navigate(..))
image of new canActivate() I have managed to solve the problem but i am still not sure why the previous implementation did not work..firebase is wierd sometimes. So I made a new observable which gets the users from firestore and i was transforming that output directly in the can activate function, see image.
Previously i got the users, transformed it and i called the transformed observable from the canactivate function wich did not seem to work. So it was something wrong with the data stream.
I have a login view which lives in its own shell. Also I have adjusted the HttpClient to automatically redirect to the login shell if any http request returns an unauthorized state.
Additionally I'd like to show some textual info to the user on the login page, after he has been "forcefully" logged out. How can I pass the information (logoutReason in the code below) from MyHttpClient to the login shell/view model?
Here's some conceptual code:
login.js
// ...
export class Login {
username = '';
password = '';
error = '';
// ...
login() {
// ... login code ...
this.aurelia.setRoot('app'); // Switch to main app shell after login succeeded...
}
// ...
}
MyHttpClient.js
// ...
export default class {
// ...
configure() {
this.httpClient.configure(httpConfig => {
httpConfig.withInterceptor({
response(res) {
if (401 === res.status) {
this.aurelia.setRoot('login');
let logoutReason = res.serversLogoutReason;
// How should i pass the logoutReason to the login shell/view model?
}
return res;
}
}});
};
// ...
}
Solution:
I've chosen to take the "event" path as suggested in bluevoodoo1's comment with some adjustments:
MyHttpClient fires/publishes a new HttpUnauthorized event which holds the needed information (description text, etc.)
MyHttpClient doesn't change the shell anymore since the concrete handling of the 401 shouldn't be his concern
login.js subscribes to the HttpUnauthorized event, changes the shell & shows the desciption text...
I'm still open to any suggestions/improvement ideas to this solution since I'm not quite sure if this is the best way to go...
You could set a localStorage or sessionStorage value and then clear it after you have displayed it. What you are asking for is known as a flash message where it displays and then expires.
Within your response interceptor add something like the following:
sessionStorage.setItem('message-logoutReason', 'Session expired, please login again');
And then in the attached method inside of your login viewmodel, check for the value and clear it, like this:
attached() {
this.error = sessionStorage.getItem('message-logoutReason');
sessionStorage.removeItem('message-logoutReason');
}
Then in your view you can display it:
${error}
As Bluevoodoo1 points out, you could also use an event, but I personally try and avoid using events as much as possible, harder to test and debug when things go wrong.