React Application Update Data from DynamoDB Change - reactjs

I am building a React application with GraphQL using AWS AppSync with DynamoDB. My use case is that I have a table of data that is being pulled from a DynamoDB table and displayed to the user using GraphQL. I have a few fields that are being updated by step functions running on AWS. I need those fields to be automatically updated for the user much like a subscription from GraphQL would do but I found out that subscriptions are tied to mutations and thus an update to the database from step functions will not trigger a subscription update on the frontend. To get around this I am using the following:
useEffect(() => {
setTimeout(getSubmissions, 5 * 1000)
})
Obviously this is a lot of overfetching and will probably incur unnecessary expense. I have looked for a better solution and come across DynamoDB streams but DynamoDB streams can't help me if they can't trigger the frontend to refresh the component. There has to be a better solution than what I have come up with.
Thanks!

You are correct, in AWS AppSync, to trigger a subscription publish you must trigger a GraphQL mutation.
but I found out that subscriptions are tied to mutations and thus an
update to the database from step functions will not trigger a
subscription update on the frontend.
If you update your DynamoDB table directly via step functions or via DynamoDB streams, then AppSync has no way to know the data refreshed.
Why don't you have your step function use an AppSync mutation instead of updating your table directly? That way you can link a subscription to the mutation and have your interested clients get pushed updates when the data is refreshed.

Assuming you are using Cognito as your authentication for your AppSync application, you could set a lambda trigger on the dynamo table that generates a cognito token, and uses that make an authorized request to your mutation endpoint. NOTE: in your cognito userpool>app clients page, you will need to check the Enable username password auth for admin APIs for authentication (ALLOW_ADMIN_USER_PASSWORD_AUTH) box to generate a client secret.
const AWS = require('aws-sdk');
const crypto = require('crypto');
var jwt = require('jsonwebtoken');
const secrets = require('./secrets.js');
var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
var config;
const adminAuth = () => new Promise((res, rej) => {
const digest = crypto.createHmac('SHA256', config.SecretHash)
.update(config.userName + config.ClientId)
.digest('base64');
var params = {
AuthFlow: "ADMIN_NO_SRP_AUTH",
ClientId: config.ClientId, /* required */
UserPoolId: config.UserPoolId, /* required */
AuthParameters: {
'USERNAME': config.userName,
'PASSWORD': config.password,
"SECRET_HASH":digest
},
};
cognitoidentityserviceprovider.adminInitiateAuth(params, function(err, data) {
if (err) {
console.log(err.stack);
rej(err);
}
else {
data.AuthenticationResult ? res(data.AuthenticationResult) : rej("Challenge requested, to verify, login to app using admin credentials");
}
});
});
const decode = auth => new Promise( res => {
const decoded = jwt.decode(auth.AccessToken);
auth.decoded = decoded
res(auth);
});
//example gql query
const testGql = auth => {
const url = config.gqlEndpoint;
const payload = {
query: `
query ListMembers {
listMembers {
items{
firstName
lastName
}
}
}
`
};
console.log(payload);
const options = {
headers: {
"Authorization": auth.AccessToken
},
};
console.log(options);
return axios.post(url, payload, options).then(data => data.data)
.catch(e => console.log(e.response.data));
};
exports.handler = async (event, context, callback) => {
await secrets() //some promise that returns your keys object (i use secrets manager)
.then( keys => {
#keys={ClientId:YOUR_COGNITO_CLIENT,
# UserPoolId:YOUR_USERPOOL_ID,
# SecretHash:(obtained from cognito>userpool>app clients>app client secret),
# gqlEndpoint:YOUR_GRAPHQL_ENDPOINT,
# userName:YOUR_COGNITO_USER,
# password:YOUR_COGNITO_USER_PASSWORD,
# }
config = keys
return adminAuth()
})
.then(auth => {
return decode(auth)
})
.then(auth => {
return testGql(auth)
})
.then( data => {
console.log(data)
callback(null, data)
})
.catch( e => {
callback(e)
})
};

Related

How to make React-Query return cached/previous data if network is not connected/available?

I've modifying some existing React Native code where I've to check if network connection is available or not. If it is available, I've to fetch new data from API, otherwise I've to use cached data. This is what I've achieved so far:
export const useStudentData = (
studentId: Student['id']
): UseQueryResult<Student, Error> => {
const queryKey = studentDataKeys.list({ studentIdEq: studentData?.id });
const queryClient = useQueryClient();
const {isConnected} = useNetInfo();
if(!isConnected){
// return previously cached data here
}
const data = useQuery<Student, Error>(
studentDataKeys.detail(studentId),
async () => {
const { data: student } = await StudentDataAPI.fetchDataById(studentId);
return StudentData.deserialize({
...studentData,
assignments: studentData.assignments?.filter((assignment) => assignment.submitted)
});
},
{
staleTime: 1000 * 60 * 60 * 10,
retry: 0,
initialData: () => {
const previousStudentData = queryClient.getQueryData<Student[]>(queryKey);
return previousStudentData?.find((studentData) => studentData.id === studentId);
},
initialDataUpdatedAt: queryClient.getQueryState(queryKey)?.dataUpdatedAt,
onError() {
console.log("Error with API fetching");
}
}
);
return data;
};
How can I modify it so that if network connection is present, it should download new data otherwise return previous/old data that was cached in previous successful call?
Instead of reinventing the wheel, you can use the existing solution. That is:
import NetInfo from '#react-native-community/netinfo'
import { onlineManager } from '#tanstack/react-query'
onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => {
setOnline(!!state.isConnected)
})
})
After implementing this, react-query will automatically refetch your data when the device is back online.
You're trying to implement a feature that React-Query provides to you for free.
React-Query will keep displaying old data until new data is available. By default, React-Query will stop trying to fetch data entirely until you are back online.
Additionally, you can set the refetchOnReconnect flag to true (which is also the default) in order to request fresh data the moment you are online.
You can rely on the queryKey for that. In detail, react-query caches the result based on the key.
Your current key is studentDataKeys.detail(studentId), what about replacing it with [studentDataKeys.detail(studentId), isConnected]?

How to navigate to the update/detail page after a POST in RTK query

I've got a form with a POST in REACT RTK-query and then it should navigate to the second step, but for that I need to know the id of the newly created record. The id is not sent in POST but AutoIncremented in the backend. I don't want to navigate to the list-view to get that id but directly.
const [addMyModel] = useNewMyModelMutation();
const handleSubmit = (e: { preventDefault: () => void; }) => {
e.preventDefault();
const my_model = {
"user_id": user_id,
"date_time_field": new Date(),
"icm": icmChoice,
...
};
dispatch(setUniqueFilters(unique_filters))
//localStorage.setItem('unique_filters', JSON.stringify(unique_filters))
//localStorage.setItem('selection_positions', JSON.stringify(positions))
//localStorage.setItem('my_model', JSON.stringify(my_model))
addMyModel(my_model)
push(`/icm/mymodels/list`) // push(`/icm/mymodels/${answer_question}`);
What could be the best solution?
try to send the id in response from the backend after POST (Django Rest Framework),
change the primary key in the backend as such that I will know the id before handleSubmit (did that before but forgot the reason why I set everything back.)
saving step1 in the localStorage (but then there will be problems with updating previous records),
saving step1 in the state (but than I cannot refresh)
It's simple, but only in case your API returns a newly created item on POST response (which is expected for RESTfull API).
If so - just await the answer from hook:
const data = await addMyModel(my_model).unwrap()
// id should be in `data`
Considering that mutation "trigger" function returns a Promise, you can use it as usual:
addMyModel(my_model).then(newItem => {
if ("data" in newItem) {
const expectedId = item.data.id;
// use you expectedId
}
});
const [addMyModel, result] = useNewMyModelMutation();
...
const handleSubmit = (e: any) => {
e.preventDefault();
const my_model = {
"user_id": user_id,
"date_time_field": new Date(),
"icm": icmChoice,
...
};
addMyModel(my_model)}
result.status === "fulfilled" && push(`/icm/mymodels/update/step2/${result.data["id"]}`)

react-query always return stale data and no call is made to server

I recently started using react-query and have encountered the issue that always stale data is returned and no call to server is made. here is the react query related code:
export function useGetAccount(id: number){
return useQuery([`account${id}`, id], async (args) => {
const [key, accountId] = args.queryKey
const [acc, teams, modules] = await Promise.all([
getAccount(),
getTeams(),
getModules()])
let account: AccountDetail = {
accountId: acc.accountId,
userId: acc.userId,
companyId: acc.companyId,
login: acc.login,
email: acc.email,
description: acc.description,
isActive: acc.isActive,
providers: acc.providers,
teams: teams,
modules: modules
}
return account
async function getAccount() {
const api = createApi() // <= axios wrapper
const { data } = await api.get(`accounts/${accountId}`, undefined, undefined)
return data as AccountModel
}
async function getTeams() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/teams`, undefined, undefined)
const { collection } = data as ResponseCollectionType<AccountTeam>
return collection
}
async function getModules() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/resources`, undefined, undefined)
const { collection } = data as ResponseCollectionType<ModuleAccessModel>
return collection
}
})
}
I even reduced the cache time but still to no avail. I do not see any calls made to server side except after a long delay or if I open the browser in incognito mode then first time the data is fetched and then no call is made.
this is used in a component which shows the details and is passed the id as a prop. everything is working fine except that the data is the one which was retrieved first time and even a refresh (F5) returns the stale data.
what changes do I need to make in this case?
[observation]: Ok, it does make a call but only after exact 5 minutes.
well the problem is not in react-query but in axios, described here Using JavaScript Axios/Fetch. Can you disable browser cache?
I used the same solution i.e. appending timestamp to the requests made by axios and everything worked fine.

React-query: how to update the cache?

I have a very basic app that fetches a user and allows to change his name. I fetch the user with React query so I can benefit from the cache feature. It works.
However, when I want to update the user, I use a classic post request with axios. Once the user is updated in the database, I need to update the cache directly in the updateUser() function. I've seen tutorials on the web that use queryCache.setCache, but it doesn't work here. How to fix this? Or maybe there is a better way to handle such queries?
Also, I notice a huge number of rendering... (see the "user render" console.log in the app file).
For the convenience, I've made a small script on a codesandbox with the pokeapi:
https://codesandbox.io/s/api-service-syxl8?file=/src/App.js:295-306
Any help would be greatly appreciated!
So, I'll show you what I do:
const updateUser = async (userUpdates: User) => {
const data = await UserService.updateUser(userUpdates); // return axios data
return data;
}
// if you want optimistic updating:
const { mutate: mutateUser } = useMutation(updateUser, {
onMutate: async (userUpdates) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['user', userUpdates.id]);
// Snapshot the previous value
const previousUser = queryClient.getQueryData(['user', userUpdates.id]);
// Optimistically update to the new value
queryClient.setQueryData(['user', userUpdates.id], userUpdates);
// Return a context with the previous user and updated user
return { previousUser, userUpdates }; // context
},
// If the mutation fails, use the context we returned above
onError: (err, userUpdates, context) => {
queryClient.setQueryData(['user', context.userUpdates.id], context.previousUser);
},
// Always refetch after error or success:
onSettled: (userUpdates) => {
queryClient.invalidateQueries(['user', userUpdates.id]);
}
});
// then to update the user
const handleUpdateUser = (userUpdates: User) => mutateUser(userUpdates);
This all comes from the docs:
Optimistic Updates

GraphQL Subscriptions with Express-GraphQL and React-Apollo

I've followed Apollo's docs for setting up GraphQL subscriptions on both the client and server, and though I'm 90% there, I can't figure out how to set up subscription channels and how to connect mutations to those channels so that whenever a mutation occurs, the server pushes the new data to the client. (For content, I'm making a Reddit clone where people post topics and others comment on it. So when you see "Topics" or "TopicList," think of those as posts.)
So far, I have set up Apollo Client for subscriptions successfully:
const wsClient = new SubscriptionClient('ws://localhost:3001/subscriptions', {
reconnect: true
});
const networkInterface = createNetworkInterface({
uri: '/graphql',
opts: {
credentials: 'same-origin'
}
});
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
dataIdFromObject: o => o.id
});
And I've set up my back-end for subscriptions as well. Here's my server.js file:
//===========================================================
//Subscription Managaer
//===========================================================
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({
schema: schema,
pubsub: pubsub
});
//=====================================
//WebSocket + Express Server
//=====================================
const server = createServer(app);
//setup listening port
server.listen(3001, ()=>{
new SubscriptionServer(
{
subscriptionManager: subscriptionManager,
onConnect: (connectionParams, webSocket) => {
console.log('Websocket connection established');
},
onSubscribe: (message, params, webSocket) => {
console.log("The client has been subscribed", message, params);
},
onUnsubsribe: (webSocket) => {
console.log("Now unsubscribed");
},
onDisconnect: (webSocket) => {
console.log('Now disconnected');
}
},
{
server: server,
path: '/subscriptions',
});
console.log('Server is hot my man!');
})
I know these are successful, because I get the "Websocket connection established" message logged in my terminal.
Next is the actual subscription - I've created a subscription schema type (just like queries and mutations):
const SubscriptionType = new GraphQLObjectType({
name: 'Subscription',
fields: () => ({
topicAdded: {
type: TopicType,
args: {repoFullName: {type: GraphQLString}}, //I don't understand what repoFullName is - I was trying to follow the Apollo docs, but they never specified that
resolve(parentValue, args){
return parentValue;
}
}
})
});
module.exports = SubscriptionType;
and incorporated it into my root schema. So when I check out GraphiQL, I see: this subscription available in the Docs side menu
My GraphiQIL UI showing the subscriptionSchema successfully
In my React component, I successfully 'subscribe' to it using Apollo's subscribeToMore method:
const TOPICS_SUBSCRIPTION = gql`
subscription OnTopicAdded($repoFullName: String){
topicAdded(repoFullName: $repoFullName){
id
}
}
`;
class TopicList extends Component {
componentDidMount() {
this.createMessageSubscription = this.props.data.subscribeToMore({
document: TOPICS_SUBSCRIPTION,
// updateQuery: (previousState, {subscriptionData}) => {
// const newTopic = subscriptionData.data.Topic.node
// const topics = previousState.findTopics.concat([newTopic])
// return {
// findTopics: topics
// }
// },
onError: (err) => console.error(err)
})
} //...
And I get my "The client has been subscribed" message logged into my terminal. But this is where I'm stuck. I've read about the SetupFunction for the SubscriptionManager, but that isn't included in Apollo's docs. And I can't find how to map a 'createTopic' mutation to this subscription so that whenever someone adds a new topic, it pops up in TopicList.
I realize this is really long, but I've been pulling my hair out tyring to figure out what's the next step. Any help would be much appreciated!! Thank you for reading!
Yes you are missing the setup function. You could take a look at this links GraphQL subscription docu or this example.
How it should work:
First you need the channel on which you publish the changed data. In your case it could look like this:
const manager = new sub.SubscriptionManager({
schema,
pubSub,
setupFunctions: {
topicAdded: (options, args) => ({ // name of your graphQL subscription
topicAddedChannel: { // name of your pubsub publish-tag
filter: (topic) => {
console.log(topic); //should always show you the new topic if there is a subscribed client
return true; // You might want to add a filter. Maybe for guest users or so
},
},
}),
},
});
And here you see the need of the args: {repoFullName: {type: GraphQLString}} argument in the subscription. If you want to filter the subscription dependent on the "repoName". Meaning that only the client with a subscription with the "repoName" as argument gets an update.
Next you need a place where you call the pubsub.publish function. In your case after the add topic mutation has passed. Could look like this:
...
const topic = new Topic(/* args */);
topic.save((error, topic) => {
if (!error) {
pubsub.publish("topicAddedChannel", topic);
}
...
});
....

Resources