How to prevent re-rendering with useSubscription()? - react-hooks

Many of you might have heard of GraphQL. It provides QUERY and MUTATION. Also it supports SUBSCRIPTION as 100% replacement of web socket. I'm a big fan of GraphQL and hooks. Today I faced an issue with useSubscription which is a hook version of SUBSCRIPTION. When this subscription is fired, a React component is re-rendered. I'm not sure why it causes re-rendering.
import React from 'react'
import { useSubscription } from 'react-apollo'
const Dashboard = () => {
...
useSubscription(query, {
onSubscriptionData: data => {
...
}
})
render (
<>
Dashboard
</>
)
}
Actually useSubscription's API doc doesn't say anything about this re-rendering now. It would be really appreciated if you provide me a good solution to preventing the re-rendering.
Thanks!

Just put your subscription in separate component as this guy did, and return null, now your root component won't rerender
function Subscription () {
const onSubscriptionData = React.useCallback(
handleSubscription, // your handler function
[])
useSubscription(CHAT_MESSAGE_SUBSCRIPTION, {
shouldResubscribe: true,
onSubscriptionData
})
return null
}
// then use this component
<Subscription />

In my experience, there is no way to prevent re-render when receiving new data in onSubscriptionData. If your global data is used for calculations, you should use useMemo for you global variable. On the other hand, you should consider do you need put your variable in onSubscriptionData? Are there other ways? Did you use useSubscription in right component? If your have to do that, you have to accept extra rendering.
Hopefully, my answer is helpful for your situation.

Related

How can I stop an Apollo GraphQL query with required exported variables from fetching when those variables are removed

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 }),
]);

class property doesn't update with redux

Context
I've built an app that renders mails from your outlook accounts into a web page in react.
I'm trying to set a "viewed" boolean as a class property fed by redux store and change it from within the component (that change must impact in redux to manage that change on the overall app )
Problem
As you might see on below's code, i initiate the instance variable in the constructor with the given information from redux reducer,
I've tested with a bunch of console logs if the action creator successfully updates that information on the store and it actually does.
My problem is that my instance variable (this.viewed) isn't updating with redux's reducer information (that actually does update)
import React from "react"
import {connect} from "react-redux"
import { bindActionCreators} from "redux"
import * as QueueActions from "../redux/actions/actionCreators/queueActions"
class Mail extends React.Component {
constructor (props){
super(props)
this.id = props.id
this.viewed = props.mails.find(mail => mail.id = this.id).viewed
}
}
componentDidMount = () => {
this.props.queueActions.setMailToViewed(this.id);
}
function mapStateToprops () {
return {
mails : store.queueReducer.mails,
}
}
function mapDispatchToProps() {
return {
queueActions : bindActionCreators( QueueActions, dispatch ),
}
}
export default connect ( mapStateToprops, mapDispatchToProps ) (Mail)
Question
what am i doing wrong here?
why does the viewed property on redux updates but my instance variable that feeds from that very same information doesn't?
shouldn't this.viewed update whenever the props that provided the information update?
Can't i update this information from props without using a state?
I think the issue is because the assignment to this.viewed happens in the constructor, which is only called once. When the redux store updates, the component will get new props but the constructor will not be called again, so the value will not be updated. Hopefully these links will help explain the issue:
ReactJS: Why is passing the component initial state a prop an anti-pattern?
https://medium.com/#justintulk/react-anti-patterns-props-in-initial-state-28687846cc2e
I'd also recommend reading up on functional components v class components and why functional components are used alot now instead of class ones. A starting point:
https://medium.com/#Zwenza/functional-vs-class-components-in-react-231e3fbd7108
If you used a functional component, you could use the useSelector hook to access the store and update your components.
Hope this this useful, I'm quite new to react so apologies if you're looking for something more, but I hope this helps.

NGXS State documentation

I'm new to NGXS and I'm trying to fully understand the docs so I can start using it knowing what I'm doing.
There is one thing I don't understand in this code snippet from here.
export class ZooState {
constructor(private animalService: AnimalService) {}
#Action(FeedAnimals)
feedAnimals(ctx: StateContext<ZooStateModel>, action: FeedAnimals) {
return this.animalService.feed(action.animalsToFeed).pipe(tap((animalsToFeedResult) => {
const state = ctx.getState();
ctx.setState({
...state,
feedAnimals: [
...state.feedAnimals,
animalsToFeedResult,
]
});
}));
}
}
Just below this code, it says:
You might notice I returned the Observable and just did a tap. If we
return the Observable, the framework will automatically subscribe to
it for us, so we don't have to deal with that ourselves. Additionally,
if we want the stores dispatch function to be able to complete only
once the operation is completed, we need to return that so it knows
that.
The framework will subscribe to this.animalService.feed, but why?
The action, FeedAnimals, uses the injected service, AnimalService to feed the animals passed in the action's payload. Presumably the service is operates asynchronously and returns an Observable. The value of that Observable is accessed via the tap function and is used to update the ZooState state context based on completing successfully.
In order to use NGXS specifically and Angular in general, you really have to understand RxJS... here's my goto doc page for it

Using NGRX in a Route Resolver

I am using Angular 6.
I am also using NGRX Store.
I am using a route guard to make sure the user is logged in to the application.
I then use a resolver to get the initial user profile, then place it in the NGRX store.
I am new to NGRX and I am not sure if this is the correct way to write the resolver.
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
return this.loginService.getLoginData()
.pipe(
map((result:UserData) => {
this.store.dispatch(new userActions.SetLoginData(result));
this.loginService.getDropdownData(
result.userId,
result.countryCode,
).subscribe( data => {
this.store.dispatch(new userActions.SetDropdownData(data));
})
})
)
}
I am also not sure if this is the correct way to do the RXJS.
any suggestions,
Thanks
I'm going to point you to Preloading ngrx/store with Route Guards, an article of Todd Motto which explains it really well.
There is also an example of the guard in the NgRx example app
#Injectable()
export class CoursesGuard implements CanActivate {
constructor(private store: Store<CoursesState>) {}
getFromStoreOrAPI(): Observable<any> {
return this.store
.select(getCoursesState)
.do((data: any) => {
if (!data.courses.length) {
this.store.dispatch(new Courses.CoursesGet());
}
})
.filter((data: any) => data.courses.length)
.take(1);
}
canActivate(): Observable<boolean> {
return this.getFromStoreOrAPI()
.switchMap(() => of(true))
.catch(() => of(false));
}
}
First of all I think it would make sense to seperate the authentication check and the data resolution into separate classes. For authentication it makes more sense to use a CanActivate guard. See: https://angular.io/api/router/CanActivate
With this out of the way your resolver can focus on only getting the data that is actually required. Here you need to be aware that if you return an observable in your resolver, the observable needs to complete in order for the resolver to complete. The problem is that if you select something from the store, the resulting observable never completes, hence your resolver will never finish resolving your data. You can work around this fact by using the first() or take(1) operator. timdeschryvers answer has a nice example on how to accomplish that.

Callback for when loading finishes from Apollo Client + React?

Im using Apollo Client 2 with React. Is there a callback for when a query has finished loading?
Im making a user's account page. The email field should display the users email. I can get this value with my graphQL query but the loading only finishes after the component has already mounted. I therefore cant use componentDidMount. Is there callback or event I use to setState and populate the email field that way?
import React from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
class AccountPage extends React.Component {
render() {
if (this.props.data.loading) return <p>Loading...</p>;
return(
<form>
<input type="email" />
</form>
)
}
}
const UserQuery = gql`
query UserQuery {
user {
_id
email
}
}
`;
export default graphql(UserQuery)(AccountPage);
There's a neat way to get around that with recompose:
const LoadingComponent = () => <p>Loading...</p>
compose(
// with compose, order matters -- we want our apollo HOC up top
// so that the HOCs below it will have access to the data prop
graphql(UserQuery),
branch(
({ data }) => {
return !data.user && data.loading
},
renderComponent(LoadingComponent)
),
// BONUS: You can define componentDidMount like normal, or you can use
// lifecycle HOC and make AccountPage a functional component :)
lifecycle({
componentDidMount() {
// set your state here
},
}),
)(AccountPage)
branch will conditionally render whatever component you pass to it instead of the wrapped component. In this case, the wrapped component is everything below branch inside of compose. That means AccountPage is not mounted until data.loading is false and data.user is truthy, which lets us use componentDidMount to set the state based on the query result.

Resources