Multiple subscriptions in AWS Amplify react Connect component - reactjs

I have a list of todo items and I would like to receive notifications when items are added or deleted from the list.
So far I have implemented item addition notification:
<Connect
query={graphqlOperation(listTodos)}
subscription={graphqlOperation(onCreateTodo)}
onSubscriptionMsg={(prev, { onCreateTodo }) => {
return addItem(prev, onCreateTodo)
}}
>
{({ data: { listTodos }, loading, error }) => {
if (loading) return "Loading"
if (error) return "Error"
return listTodos.items
.map(({ id, name }) => <div key={id}>{name}</div>)
}}
</Connect>
Now I am wondering, how can I add item deletion notification to this component? Does subscription attribute accept an array of graphql operations?
Thanks!

You can use multiple Connect components in your component.
<Connect query={graphqlOperation(listTodos)}>
{({ data: { listTodos }, loading, error }) => {
if (loading) return "Loading"
if (error) return "Error"
return listTodos.items.map(({ id, name }) => <div key={id}>{name}</div>)
}}
</Connect>
<Connect
subscription={graphqlOperation(subscriptions.onCreateTodo)}
onSubscriptionMsg={(prev, { onCreateTodo }) => {
// Do something
return prev;
}}
>
{() => {}}
</Connect>
<Connect
subscription={graphqlOperation(subscriptions.onUpdateTodo)}
onSubscriptionMsg={(prev, { onUpdateTodo }) => {
// Do something
return prev;
}}
>
{() => {}}
</Connect>

Looks like the current solution is to create your own implementation of the Connect component as described in this github issue: https://github.com/aws-amplify/amplify-js/issues/4813#issuecomment-582106596

I tried the Connect.ts version above and got the same errors reported by others in this thread. So I created a version whereby you can pass in multiple subscriptions as an array - you can still pass in single a subscription too - as per the original version. Note: this version takes only a single query and a single onSubscriptionMessage - however your onSubscriptionMessage can be a wrapper function that examines the newData passed into it and calls the appropriate update depending on this data like this:
const onSubscriptionMessage = (prevQuery, newData) => {
if(newData && newData.onDeleteItem) {
return onRemoveItem(prevQuery, newData);
}
if(newData && newData.onCreateItem) {
return onAddItem(prevQuery, newData);
}
};
Connect.ts for multiple subscriptions given a single query and a single onSubscriptionMessage handler that switches handling according to the newData.
import * as React from 'react';
import { API, GraphQLResult } from '#aws-amplify/api';
import Observable from 'zen-observable-ts';
export interface IConnectProps {
mutation?: any;
onSubscriptionMsg?: (prevData: any) => any;
query?: any;
subscription?: any;
}
export interface IConnectState {
loading: boolean;
data: any;
errors: any;
mutation: any;
}
export class Connect extends React.Component<IConnectProps, IConnectState> {
public subSubscriptions: Array<Promise<GraphQLResult<object>> | Observable<object>>;
private mounted: boolean = false;
constructor(props:any) {
super(props);
this.state = this.getInitialState();
this.subSubscriptions = [];
}
getInitialState() {
const { query } = this.props;
return {
loading: query && !!query.query,
data: {},
errors: [],
mutation: () => console.warn('Not implemented'),
};
}
getDefaultState() {
return {
loading: false,
data: {},
errors: [],
mutation: () => console.warn('Not implemented'),
};
}
async _fetchData() {
this._unsubscribe();
this.setState({ loading: true });
const {
// #ts-ignore
query: { query, variables = {} } = {},
//#ts-ignore
mutation: { query: mutation,
//eslint-disable-next-line
mutationVariables = {} } = {},
subscription,
onSubscriptionMsg = (prevData:any) => prevData,
} = this.props;
let { data, mutation: mutationProp, errors } = this.getDefaultState();
if (
!API ||
typeof API.graphql !== 'function' ||
typeof API.getGraphqlOperationType !== 'function'
) {
throw new Error(
'No API module found, please ensure #aws-amplify/api is imported'
);
}
const hasValidQuery =
query && API.getGraphqlOperationType(query) === 'query';
const hasValidMutation =
mutation && API.getGraphqlOperationType(mutation) === 'mutation';
const validSubscription = (subscription:any) => subscription
&& subscription.query
&& API.getGraphqlOperationType(subscription.query) === 'subscription';
const validateSubscriptions = (subscription:any) => {
let valid = false;
if(Array.isArray(subscription)) {
valid = subscription.map(validSubscription).indexOf(false) === -1;
} else {
valid = validSubscription(subscription)
}
return valid;
};
const hasValidSubscriptions = validateSubscriptions(subscription);
if (!hasValidQuery && !hasValidMutation && !hasValidSubscriptions) {
console.warn('No query, mutation or subscription was specified correctly');
}
if (hasValidQuery) {
try {
// #ts-ignore
data = null;
const response = await API.graphql({ query, variables });
// #ts-ignore
data = response.data;
} catch (err) {
data = err.data;
errors = err.errors;
}
}
if (hasValidMutation) {
// #ts-ignore
mutationProp = async variables => {
const result = await API.graphql({ query: mutation, variables });
this.forceUpdate();
return result;
};
}
if (hasValidSubscriptions) {
// #ts-ignore
const connectSubscriptionToOnSubscriptionMessage = (subscription) => {
// #ts-ignore
const {query: subsQuery, variables: subsVars} = subscription;
try {
const observable = API.graphql({
query: subsQuery,
variables: subsVars,
});
// #ts-ignore
this.subSubscriptions.push(observable.subscribe({
// #ts-ignore
next: ({value: {data}}) => {
const {data: prevData} = this.state;
// #ts-ignore
const newData = onSubscriptionMsg(prevData, data);
if (this.mounted) {
this.setState({data: newData});
}
},
error: (err:any) => console.error(err),
}));
} catch (err) {
errors = err.errors;
}
};
if(Array.isArray(subscription)) {
subscription.forEach(connectSubscriptionToOnSubscriptionMessage);
} else {
connectSubscriptionToOnSubscriptionMessage(subscription)
}
}
this.setState({ data, errors, mutation: mutationProp, loading: false });
}
_unsubscribe() {
const __unsubscribe = (subSubscription:any) => {
if (subSubscription) {
subSubscription.unsubscribe();
}
};
this.subSubscriptions.forEach(__unsubscribe);
}
async componentDidMount() {
this._fetchData();
this.mounted = true;
}
componentWillUnmount() {
this._unsubscribe();
this.mounted = false;
}
componentDidUpdate(prevProps:any) {
const { loading } = this.state;
const { query: newQueryObj, mutation: newMutationObj, subscription: newSubscription} = this.props;
const { query: prevQueryObj, mutation: prevMutationObj, subscription: prevSubscription } = prevProps;
// query
// #ts-ignore
const { query: newQuery, variables: newQueryVariables } = newQueryObj || {};
// #ts-ignore
const { query: prevQuery, variables: prevQueryVariables } =
prevQueryObj || {};
const queryChanged =
prevQuery !== newQuery ||
JSON.stringify(prevQueryVariables) !== JSON.stringify(newQueryVariables);
// mutation
// #ts-ignore
const { query: newMutation, variables: newMutationVariables } =
newMutationObj || {};
// #ts-ignore
const { query: prevMutation, variables: prevMutationVariables } =
prevMutationObj || {};
const mutationChanged =
prevMutation !== newMutation ||
JSON.stringify(prevMutationVariables) !==
JSON.stringify(newMutationVariables);
// subscription
// #ts-ignore
const { query: newSubsQuery, variables: newSubsVars } = newSubscription || {};
// #ts-ignore
const { query: prevSubsQuery, variables: prevSubsVars } = prevSubscription || {};
const subscriptionChanged =
prevSubsQuery !== newSubsQuery ||
JSON.stringify(prevSubsVars) !==
JSON.stringify(newSubsVars);
if (!loading && (queryChanged || mutationChanged || subscriptionChanged)) {
this._fetchData();
}
}
render() {
const { data, loading, mutation, errors } = this.state;
// #ts-ignore
return this.props.children({ data, errors, loading, mutation }) || null;
}
}
/**
* #deprecated use named import
*/
export default Connect;
Usage: an example of onSubscriptionMessage is at the top of this post.
<Connect
query={graphqlOperation(listTopics)}
subscription={[graphqlOperation(onCreateTopic), graphqlOperation(onDeleteTopic)]}
onSubscriptionMsg={onSubscriptionMessage}>
{.....}
</Connect>

Related

TypeError: Cannot read properties of undefined using reactjs and graphql

I'm creating a social media using RactJS, Mongoose, GraphQL and Apollo. When I try to fetch data from DB to be displayed in the home page I get an error saying TypeError: Cannot read properties of undefined (reading 'getPosts'). I search for the solution and tried a bunch of things and nothing worked
File structre
--client
----node_modules
----public
----src
------components
--------MenuBar.js
--------PostCard.js
------pages
--------Home.js
--graphQL
----resolvers
--------comments.js
--------index.js
--------posts.js
--------users.js
----typeDefs.js
--models
----Post.js
----User.js
Home.js
import React from 'react';
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { Grid } from 'semantic-ui-react';
import PostCard from '../components/PostCard';
function Home() {
const { loading, data: { getPosts: posts } } = useQuery(FETCH_POSTS_QUERY);
return (
<Grid columns={3}>
<Grid.Row>
<h1>Recent Posts</h1>
</Grid.Row>
<Grid.Row>
{loading ? (
<h1>Loading posts..</h1>
) : (
posts && posts.map(post => (
<Grid.Column key={post.id}>
<PostCard post={post} />
</Grid.Column>
))
)}
</Grid.Row>
</Grid>
)
}
const FETCH_POSTS_QUERY = gql`
{
getPosts{
id
body
createdAt
username
likeCount
likes {
username
}
commentCount
comments{
id
username
createdAt
body
}
}
}
`;
export default Home;
posts.js
const { AuthenticationError, UserInputError } = require('apollo-server');
const Post = require('../../models/Post');
const checkAuth = require('../../util/check-auth');
module.exports = {
Query: {
async getPosts() {
try {
const posts = await Post.find().sort({ createdAt: -1 });
return posts;
} catch (err) {
throw new Error(err);
}
},
async getPost(_, { postId }) {
try {
const post = await Post.findById(postId);
if(post) {
return post;
} else {
throw new Error('Post not found')
}
} catch (err) {
throw new Error(err);
}
}
},
Mutation: {
async createPost(_, { body }, context) {
const user = checkAuth(context);
if(args.body.trim() === '') {
throw new Error('Post body must not be empty');
}
const newPost = new Post({
body,
user: user.id,
username: user.username,
createdAt: new Date().toISOString()
});
const post = await newPost.save();
context.pubsub.publish('NEW_POST', {
newPost: post
});
return post;
},
async deletePost(_, { postId }, context) {
const user = checkAuth(context);
try {
const post = await Post.findById(postId);
if(user.username === post.username) {
await post.delete();
return 'Post deleted successfully';
} else {
throw new AuthenticationError('Action not allowed');
}
} catch (err) {
throw new Error(err);
}
},
async likePost(_, { postId }, context) {
const { username } = checkAuth(context);
const post = await Post.findById(postId);
if (post) {
if (post.likes.find((like) => like.username === username)) {
// Post already likes, unlike it
post.likes = post.likes.filter((like) => like.username !== username);
} else {
// Not liked, like post
post.likes.push({
username,
createdAt: new Date().toISOString()
});
}
await post.save();
return post;
} else throw new UserInputError('Post not found');
}
},
Subscription: {
newPost: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator('NEW_POST')
}
}
}
When I console.log(data) before implementing the data: {getPosts: posts} it runned smoothly returning an object as expected but after that the app crashed.
maybe try this:
const { loading, data } = useQuery(FETCH_POSTS_QUERY)
const { getPosts: posts } = {...data}
or another way is to do this:
const { loading, data: { getPosts: posts } = {} } = useQuery(FETCH_POSTS_QUERY)
Let me know if it still doesn't work leave a comment below.
instead of using
const { loading, data: { getPosts: posts } } = useQuery(FETCH_POSTS_QUERY)
try this:
const { loading, data } = useQuery(FETCH_POSTS_QUERY)
const posts = data?.getPosts

ReactJS | How do I get the response when the API hit process is successful?

This is function when hit api
function click to triger
const _selectBranch = async () => {
const { user, } = props;
await props.fetchGetBranchList(user.tokenResponse.accessToken, user.customerCode);
const { branchList } = props;
console.log(branchList)
}
this action dispatch
export const getBranchList = (token, code) => async (dispatch) => {
dispatch({
type: BRANCH_LIST_REQ,
});
try {
const res = await apiBranch(token).listBranchGet(code);
if(res.data != null){
dispatch({
type: BRANCH_LIST_SUCCESS,
payload: { data: res.data },
});
}
} catch (error) {
dispatch({
type: BRANCH_LIST_FAILED,
payload: error
});
}
};
my reducers
const initialState = {
branchList: null,
};
export const listNews = (state = { ...initialState }, action) => {
const { payload, type } = action;
switch (type) {
case BRANCH_LIST_REQ: {
return {
...state,
branchList: null
};
}
case BRANCH_LIST_SUCCESS: {
const { data } = payload;
return {
...state,
branchList: data
};
}
default:
return state;
}
};
I succeeded in getting a response, if I use useEffect
this code
useEffect(() => {
if(props.branchList){
setStorage("PO_ACTIVE", purchaseOrder);
props.push('/Home/BucketList/', props.branchList.cartId)
}
},[props.branchList]);
but I want to get a response in the process of this code, but if I use this code the response I get is always null
const _selectBranch = async () => {
const { user, } = props;
await props.fetchGetBranchList(user);
const { branchList } = props;
if(branchList != null ){
setStorage("PO_ACTIVE", purchaseOrder);
props.push('/Home/BucketList/', props.branchList.cartId)
}
console.log(branchList)
}

LocalStorage not updating property in state with React hooks

I'm trying to update an object property previously declared in a useState hook for form values and save it in localstorage. Everything goes well, but localstorage is saving date property empty all the time, I know that it must be because of asynchrony but I can't find the solution. This is my code. I'm newbie with React hooks. Lot of thanks!
const [formValues,setformValues] = useState(
{
userName:'',
tweetText:'',
date:''
}
)
const getlocalValue = () => {
const localValue = JSON.parse(localStorage.getItem('tweetList'));
if(localValue !== null){
return localValue
} else {
return []
}
}
const [tweetList,setTweetList] = useState(getlocalValue());
const handleInput = (inputName,inputValue) => {
setformValues((prevFormValues) => {
return {
...prevFormValues,
[inputName]:inputValue
}
})
}
const handleForm = () => {
const {userName,tweetText} = formValues;
if(!userName || !tweetText) {
console.log('your tweet is empty');
} else {
setformValues(prevFormValues => {
return {
...prevFormValues,
date:getCurrentDate() //this is not updating in local
}
})
setTweetList(prevTweets => ([...prevTweets, formValues]));
toggleHidden(!isOpen)
}
}
console.log(formValues) //but you can see changes outside the function
useEffect(() => {
localStorage.setItem('tweetList', JSON.stringify(tweetList));
}, [tweetList]);
In this case the issue is because the handleForm that was called still only has access to the formValues state at the time it was called, rather than the new state. So, the easiest way to handle this is to just update the formValues, setFormValues, and then setTweetList based on the local copy of the updated formValues.
const handleForm = () => {
const {userName,tweetText} = formValues;
if(!userName || !tweetText) {
console.log('your tweet is empty');
} else {
const updatedFormValues = {...formValues,date:getCurrentDate()};
setformValues(updatedFormValues)
setTweetList(prevTweets => ([...prevTweets, updatedFormValues]));
toggleHidden(!isOpen)
}
}
Since there's issues with concurrency here: i.e. you can't guarantee an update to the state of both formValues and tweetList with the latest data. Another option is useReducer instead of the two separate state variables because they are related properties and you'd be able to update them based off of each other more easily.
As an example of making more complicated updates with reducers, I added a 'FINALIZE_TWEET' action that will perform both parts of the action at once.
const Component = () => {
const [{ formValues, tweetList }, dispatch] = useReducer(
reducer,
undefined,
getInitState
);
const handleInput = (inputName, inputValue) => {
dispatch({ type: 'SET_FORM_VALUE', payload: { inputName, inputValue } });
};
const handleForm = () => {
const { userName, tweetText } = formValues;
if (!userName || !tweetText) {
console.log('your tweet is empty');
} else {
dispatch({ type: 'SET_FORM_DATE' });
dispatch({ type: 'PUSH_TO_LIST' });
// OR
// dispatch({type: 'FINALIZE_TWEET'})
toggleHidden(!isOpen);
}
};
console.log(formValues); //but you can see changes outside the function
useEffect(() => {
localStorage.setItem('tweetList', JSON.stringify(tweetList));
}, [tweetList]);
return <div></div>;
};
const getlocalValue = () => {
const localValue = JSON.parse(localStorage.getItem('tweetList'));
if (localValue !== null) {
return localValue;
} else {
return [];
}
};
function getInitState() {
const initialState = {
formValues: {
userName: '',
tweetText: '',
date: '',
},
tweetList: getlocalValue(),
};
}
function reducer(state, action) {
switch (action.type) {
case 'SET_FORM_VALUE':
return {
...state,
formValues: {
...state.formValues,
[action.payload.inputName]: action.payload.inputValue,
},
};
case 'SET_FORM_DATE':
return {
...state,
formValues: {
...state.formValues,
date: getCurrentDate(),
},
};
case 'PUSH_TO_LIST':
return {
...state,
tweetList: [...state.tweetList, state.formValues],
};
case 'FINALIZE_TWEET': {
const newTweet = {
...state.formValues,
date: getCurrentDate(),
};
return {
...state,
formValues: newTweet,
tweetList: [...state.tweetList, newTweet],
};
}
default:
return state;
}
}

TypeError: unsubFirebaseSnapShot01 is not a function

I'm building a web app with React.
On the Home screen, I've the following code.
When running the app, it throws TypeError: unsubFirebaseSnapShot01 is not a function.
browser console error screenshot:
These are the only references to these functions in index.js:
let unsubFirebaseSnapShot01;
let unsubFirebaseSnapShot02;
let unsubFirebaseSnapShot03;
...
class Home extends Component {
...
componentWillUnmount() {
this.mounted = false;
unsubFirebaseSnapShot01();
unsubFirebaseSnapShot02();
unsubFirebaseSnapShot03();
}
checkAll = () => {
this.checkMeetingsSetByMe();
this.checkMeetingsSetForMe();
this.checkNotifications();
};
checkMeetingsSetByMe = () => {
if (!this.mounted) return;
const {
user: { uid },
} = this.props;
this.setState({ screenLoading: true });
unsubFirebaseSnapShot01 = Meetings.where('setBy', '==', uid).onSnapshot(
({ docs }) => {
// setting all the meetingsSetByMe in one place
const meetingsSetByMe = docs.map(item => item.data());
if (this.mounted);
this.setState({ meetingsSetByMe, screenLoading: false });
},
);
};
checkMeetingsSetForMe = () => {
if (!this.mounted) return;
const {
user: { uid },
} = this.props;
this.setState({ screenLoading: true });
unsubFirebaseSnapShot02 = Meetings.where('setWith', '==', uid).onSnapshot(
({ docs }) => {
// setting all the meetingsSetForMe in one place
const meetingsSetForMe = docs.map(item => item.data());
if (this.mounted);
this.setState({ meetingsSetForMe, screenLoading: false });
},
);
};
checkNotifications = () => {
if (!this.mounted) return;
const {
user: { uid },
} = this.props;
this.setState({ screenLoading: true });
unsubFirebaseSnapShot03 = Meetings.where('status', '==', 'unseen')
.where('setWith', '==', uid)
.onSnapshot(({ docs }) => {
const notifications = docs.map(item => item.data());
if (this.mounted);
this.setState({ notifications, screenLoading: false });
});
};
Double checked any typos but cannot see my mistake.
Any help?

How to fix 'Can't perform a React state...' error in React

I making mutation in LyricCreate
` onSubmit = (e) => {
e.preventDefault();
const { content } = this.state;
const { songId, addLyric } = this.props;
addLyric({
variables: {
content,
songId
},
}).then( () => this.setState({ content: '' }) )
}`
it's going well, and adds to database.
But in parent component appears error with
after refresh page created Lyric appears in lyricList, and parent component songDetails doesn't has errors till I make mutation again.
Help please..
you can check if your component is mounted like this
componentDidMount() {
this._ismounted = true;
}
componentWillUnmount() {
this._ismounted = false;
}
onSubmit = (e) => {
e.preventDefault();
const { content } = this.state;
const { songId, addLyric } = this.props;
addLyric({
variables: {
content,
songId
},
}).then(() => {
if(this._ismounted {
this.setState({ content: '' })
}
})
}

Resources