I'm new to graphql I'm building real time chat app. Currently I'm making it offline first.
Using react as front-end.
I'm currently caching the data on localStorage using apollo3-cache-persist. But How do I query the cache data instead of server (when I'm offline) also I want to add messages to the localStorage while I'm offline.
Display the optimistic response when the device is online I want to send the pending data to the server.
my ApolloProvider.js file in client folder
import React from "react";
import {
ApolloClient,
InMemoryCache,
ApolloProvider as Provider,
createHttpLink,
ApolloLink,
split,
} from "#apollo/client";
import { setContext } from "#apollo/client/link/context";
import { RetryLink } from "#apollo/client/link/retry";
import { persistCache, LocalStorageWrapper } from "apollo3-cache-persist";
import { WebSocketLink } from "#apollo/client/link/ws";
import { getMainDefinition } from "#apollo/client/utilities";
import QueueLink from "apollo-link-queue";
let httpLink = createHttpLink({
uri: "http://localhost:4000/",
});
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("token");
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
});
httpLink = authLink.concat(httpLink);
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: {
reconnect: true,
connectionParams: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
},
});
const link = new RetryLink();
const queueLink = new QueueLink();
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const cache = new InMemoryCache();
const fun = async () =>
await persistCache({
cache,
storage: new LocalStorageWrapper(window.localStorage),
});
fun();
const client = new ApolloClient({
// link: splitLink,
link: ApolloLink.from([splitLink, queueLink, link]),
cache,
name: "chat-app",
version: "1.0.0",
queryDeduplication: false,
defaultOptions: {
watchQuery: {
fetchPolicy: "cache-and-network",
},
},
});
export default function ApolloProvider(props) {
return <Provider client={client} {...props} />;
}
my messages.js file
import React, { Fragment, useEffect, useState } from "react";
import { gql, useLazyQuery, useMutation, InMemoryCache } from "#apollo/client";
import { Col, Form } from "react-bootstrap";
import { useMessageDispatch, useMessageState } from "../../context/message";
import uuid from "react-uuid";
import Message from "./Message";
const SEND_MESSAGE = gql`
mutation sendMessage($uuid: String, $to: String!, $content: String!) {
sendMessage(uuid: $uuid, to: $to, content: $content) {
uuid
from
to
content
createdAt
hasSeen
hasSent
}
}
`;
const GET_MESSAGES = gql`
query getMessages($from: String!) {
getMessages(from: $from) {
uuid
from
to
content
createdAt
hasSeen
}
}
`;
export default function Messages() {
const { users } = useMessageState();
const dispatch = useMessageDispatch();
const [content, setContent] = useState("");
const selectedUser = users?.find((u) => u.selected === true);
const messages = selectedUser?.messages;
const [getMessages, { loading: messagesLoading, data: messagesData }] =
useLazyQuery(GET_MESSAGES, {
update(cache) {
cache.readFragment({});
console.log("reading");
},
});
const [sendMessage] = useMutation(SEND_MESSAGE, {
update(cache, { data: { sendMessage } }) {
cache.modify({
fields: {
getMessages(existingMsg) {
console.log(existingMsg);
const newMsgRef = cache.writeFragment({
data: sendMessage,
fragment: gql`
fragment sendNewMessage on Mutation {
uuid
to
from
content
hasSeen
hasSent
}
`,
});
return existingMsg.push(newMsgRef);
},
},
});
},
onError: (err) => console.log(err),
});
useEffect(() => {
if (selectedUser && !selectedUser.messages) {
getMessages({ variables: { from: selectedUser.username } });
}
}, [selectedUser]);
useEffect(() => {
if (messagesData) {
dispatch({
type: "SET_USER_MESSAGES",
payload: {
username: selectedUser.username,
messages: messagesData.getMessages,
},
});
}
}, [messagesData]);
const submitMessage = (e) => {
e.preventDefault();
if (content.trim() === "" || !selectedUser) return;
let id = uuid();
sendMessage({
variables: { uuid: id, to: selectedUser.username, content },
optimisticResponse: {
sendMessage: {
__typename: "Mutation",
uuid: id,
from: "User",
to: selectedUser.username,
content,
hasSent: false,
hasSeen: false,
createdAt: Date.now(),
},
},
});
setContent("");
};
// Displaying helper text and styling
let selectedChatMarkup;
if (!messages && !messagesLoading) {
selectedChatMarkup = <p className="info-text"> Select a friend</p>;
} else if (messagesLoading) {
selectedChatMarkup = <p className="info-text"> Loading..</p>;
} else if (messages.length > 0) {
selectedChatMarkup = messages.map((message, index) => (
<Fragment key={message.uuid}>
<Message message={message} />
{index === messages.length - 1 && (
<div className="invisible">
<hr className="m-0" />
</div>
)}
</Fragment>
));
} else if (messages.length === 0) {
selectedChatMarkup = (
<p className="info-text">
You are now connected! send your first message!
</p>
);
}
return (
<Col xs={10} md={8}>
<div className="messages-box d-flex flex-column-reverse">
{selectedChatMarkup}
</div>
<div>
<Form onSubmit={submitMessage}>
<Form.Group className="d-flex align-items-center">
<Form.Control
type="text"
className="message-input rounded-pill p-4 bg-secondary border-0"
placeholder="Type a message.."
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<i
className="fas fa-regular fa-paper-plane fa-2x text-primary ml-2"
onClick={submitMessage}
role="button"
></i>
</Form.Group>
</Form>
</div>
</Col>
);
}
But I'm currently getting this error when I try to send the message
react_devtools_backend.js:3973 Invariant Violation: Could not identify object {"uuid":"4855ffc-6b7b-d7c8-a68-2ae0162f80a","from":"User","to":"Fire","content":"example text","createdAt":1648881891383,"hasSeen":false,"hasSent":false,"__typename":"Mutation"}
Also getting error from the mutation error log
Error: Could not identify object {"__typename":"Message","uuid":"4855ffc-6b7b-d7c8-a68-2ae0162f80a","from":"Alan","to":"Fire","content":"example text","createdAt":"2022-04-02T06:44:51.807Z","hasSeen":false,"hasSent":false}
Apollo uses by default __typename and id fields to normalise cache. So in your case, Apollo won't recognise your uuid property as a cache identifier. You can change the uuid uuid property, or add keyFields to your InMemoryCache config.
Related
I am using firebase-ui-react in a React app for authentication, but the problem is that if someone open the app and don't do any activity up to an hour and after one hour click on a link, all the endpoints get called several times until the the app crash.
the index.jsx looks like this:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route } from "react-router-dom";
import { Provider } from "react-redux";
import * as Sentry from "#sentry/react";
import store from "./Redux/store";
import App from "./App";
import ReactGA from "react-ga";
import { AuthProvider } from "./Containers/Auth/AuthProvider";
import * as firebaseconfig from "./deployment_config.json";
const fbconfig =
firebaseconfig.default[process.env.REACT_APP_ENV_NAME][
process.env.REACT_APP_CLIENT
];
Sentry.init({ dsn: fbconfig.SENTRY_DSN });
ReactGA.initialize(fbconfig.GOOGLE_ANALYTICS_TRACKING);
ReactDOM.render(
<BrowserRouter>
<AuthProvider fbconfig={fbconfig}>
<Provider store={store}>
<App />
</Provider>
</AuthProvider>
</BrowserRouter>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
the App.jsx looks like:
import React, { useState, useContext, useEffect } from "react";
import { AuthContext } from "./Containers/Auth/AuthProvider";
import { withRouter } from "react-router-dom";
import { Provider, useDispatch, useSelector } from "react-redux";
//lots of other imports
const App = (props) => {
const { location } = props;
const { user, accessTokenExpired, loading, userRole, uiConfig } = useContext(
AuthContext
);
toast.configure();
const [checkSession, setCheckSession] = useState(false);
const [modal, setModal] = useState(true);
const change_body = useSelector((state) => state.Common.change_body);
const ErrorStatusState = useSelector(
(state) => state.GetGlobalError.ErrorCode
);
const configdata = useSelector((state) => state.GetConfig.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(GetConfig());
}, []);
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.message === "Network Error") {
setCheckSession(true);
}
return Promise.reject(error);
}
);
if (loading) {
return <PageLoader />;
}
if (user === null) {
return <Login uiConfig={uiConfig} user={user} userRole={userRole} />;
}
if (userRole === null) {
return <Verification />;
}
if (ErrorStatusState > 0 || configdata.maintenance) {
return (
<>
<Provider store={store}>
<GlobalStyle />
<Header />
<ContentWrapper>
<MainContentWrapper>
<AppWrapper>
<PageWrapper>
<MainWrapper>
<ErrorClassifyRender
ErrorStatus={ErrorStatusState}
maintenance={configdata.maintenance}
/>
</MainWrapper>
</PageWrapper>
</AppWrapper>
</MainContentWrapper>
</ContentWrapper>
</Provider>
</>
);
}
return (
<Provider store={store}>
<GlobalStyle />
<Header />
<ContentWrapper>
<MainContentWrapper>
<Navigation />
<AppWrapper>
<MainApp id="appDiv">
<PageWrapper>
<MainWrapper>
<Routes />
</MainWrapper>
</PageWrapper>
</MainApp>
</AppWrapper>
</MainContentWrapper>
</ContentWrapper>
</Provider>
);
};
export default withRouter(App);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
the AuthProvider looks like:
import { useEffect, createContext, useState } from "react";
import { isAfter } from "date-fns";
import firebase from "firebase/app";
import axios from "axios";
export const AuthContext = createContext();
export const AuthProvider = ({ children, fbconfig }) => {
const baseURL = process.env.REACT_APP_BACKEND_URL || window.location.origin;
const [user, setUser] = useState(null);
const [userRole, setUserRole] = useState(null);
const [isEmailVerified, setIsEmailVerified] = useState(false);
const [loading, setLoading] = useState(true);
const [verificationLoading, setVerificationLoading] = useState(false);
const [verifyRequestBlocked, setVerifyRequestBlocked] = useState(false);
const [emailSent, setEmailSent] = useState(false);
const [isNewUser, setIsNewUser] = useState(false);
const [accessTokenExpired, setAccessTokenExpired] = useState(false);
// Configure Firebase.
const config = {
apiKey: fbconfig.FIREABSE_API_KEY,
authDomain: fbconfig.FIREABSE_AUTH_DOMAIN,
projectId: fbconfig.FIREABSE_PROJECT_ID,
};
// Configure FirebaseUI.
if (!firebase.apps.length) {
firebase.initializeApp(config);
}
const sendEmail = async ({ email, isNewUser = false }) => {
// setLoading(true);
setVerificationLoading(true);
await axios
.post(`${baseURL}/send_email_verification`, {
email,
link: window.location.origin,
})
.then((response) => {
if (response.data.error) {
setUser(response);
setEmailSent(true);
setVerifyRequestBlocked(true);
setVerificationLoading(false);
} else {
setUser(response);
setEmailSent(true);
setVerificationLoading(false);
}
})
.catch((error) => {
setVerifyRequestBlocked(true);
setUser({ data: { email: email } });
setVerificationLoading(false);
setEmailSent(true);
});
setIsNewUser(isNewUser);
};
const uiConfig = {
signInFlow: "popup",
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
signInSuccessUrl: "/",
callbacks: {
signInSuccessWithAuthResult: async (authResult) => {
if (authResult.additionalUserInfo.isNewUser) {
await sendEmail({
isNewUser: true,
email: authResult.user.email,
});
} else {
setIsNewUser(false);
setLoading(false);
}
return false;
},
},
};
useEffect(() => {
firebase.auth().onAuthStateChanged(async (user) => {
setLoading(true);
if (user === null) {
setUser(null);
setLoading(false);
return;
}
if (user.emailVerified) {
setIsEmailVerified(user.emailVerified);
await user.getIdToken().then(async (accessToken) => {
setLoading(true);
try {
const result = await axios.post(
`${baseURL}/user`,
{
token: accessToken,
},
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const expirationTimeInSeconds =
JSON.parse(atob(accessToken.split(".")[1])).exp * 1000;
const dateResult = isAfter(
new Date(),
new Date(expirationTimeInSeconds)
);
if (dateResult) {
setAccessTokenExpired(true);
}
localStorage.setItem("token", accessToken);
localStorage.setItem("user", JSON.stringify(result.data));
setUser({
...result.data,
token: accessToken,
status: result.status,
});
setUserRole(result.data.role);
setLoading(false);
} catch (error) {
setUser(null);
setLoading(false);
}
});
} else {
setUser({ data: user });
setLoading(false);
}
});
}, []);
return (
<AuthContext.Provider
value={{
user,
accessTokenExpired,
emailSent,
verifyRequestBlocked,
isEmailVerified,
loading,
userRole,
uiConfig,
isNewUser,
verificationLoading,
sendEmail,
}}
>
{children}
</AuthContext.Provider>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
and one of the endpoint calls are like:
import React, { useRef, useState, useEffect } from "react";
import styled from "styled-components";
import { useSelector, useDispatch } from "react-redux";
import ReactEcharts from "echarts-for-react";
import {
fetchHistory,
} from "../../Redux";
const History = (props) => {
const barRef = useRef();
const {
brand,
parentStartDate,
parentEndDate,
start_date,
end_date,
selectedId,
tabID,
showTabs,
showComponent,
detailed,
graphs,
tooltipExtra,
} = props;
const predict_data_response = useSelector((state) => state.History.predict_data);
useEffect(() => {
dispatch(
fetchHistoryroi({
brand: brand.name,
parentStartDate,
parentEndDate,
componentName,
tabID,
nestedObjects,
})
);
}, [parentStartDate, parentEndDate]);
const predict_data = ResponseFilter({ responseFilterProps });
const options = {
toolbox: getToolBox(),
textStyle: {
color: colors.white,
},
tooltip: {
formatter: (params) => {
return getTooltip({
params,
avarage,
devider,
componentName: "History",
});
},
backgroundColor: colors.cardBg,
extraCssText: extraCssText,
},
grid: {
left: "7px",
right: "7px",
bottom: "3%",
containLabel: true,
},
xAxis: [
{
axisLabel: {
fontSize: 10,
},
type: "category",
data: labels,
axisTick: {
alignWithLabel: true,
},
},
{
axisLabel: {
fontSize: 10,
},
position: "bottom",
offset: 15,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
data: labelsValues,
},
],
yAxis: [
{
axisLabel: {
fontSize: 10,
formatter: (value, index) => {
return value;
},
},
splitLine: {
lineStyle: {
color: colors.lightGray,
},
},
type: "value",
},
],
series: [
{
type: "bar",
label: {
show: true,
position: ["30%", "30%"],
formatter: ({ data }) => {
const { value } = data;
return value === avarage / devider && avarage !== 0 ? "NA" : "";
},
},
barMaxWidth: 40,
data: nums,
},
],
};
return (
<ContainerWrapper>
<ReactEcharts
ref={barRef}
option={options}
data-test="HistoryGraph"/>
</ContainerWrapper>
);
};
export default History;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
this is the image on what happens when someone click a link after an hour inactiviy
any help please?
Instead of parsing the access token directly, the Firebase SDK provides User#getIdTokenResult() which provides the metadata of the token along with the token itself.
const { token, expirationTime } = await firebase.auth().currentUser.getIdTokenResult();
Instead of storing the access token in the object you are passing to setUser(), a better option would be to add a Axios request interceptor, like you are doing for the responses. This will allow you to trigger requests using Axios as you normally would, but you don't have to add { headers: { Authorization: `Bearer ${accessToken}` } } on every request.
The following useEffect() attaches an interceptor on mount and detaches it on unmount, where the interceptor attaches the user's latest ID token to the request. Here, I've also added a clause where if the token were to expire in the next 5 minutes, it's forcefully refreshed.
useEffect(() => {
const authRequestInterceptor = axios.interceptors.request.use(async (config) {
if (!config.url.startsWith(baseURL))
return config; // this request doesn't target our API, leave untouched
const currentUser = firebase.auth().currentUser;
if (currentUser === null)
return config; // no user signed in, return config unmodified
// getIdTokenResult provides token metadata in the result
// will return current token, unless it's expired where a fresh token is grabbed
let { token, expirationTime } = await currentUser.getIdTokenResult();
const expiresMS = (new Date(expirationTime)).getTime();
if (expiresMS - Date.now() < 300000) {
// if the token will expire in the next 5 minutes,
// forcefully grab a fresh token
token = await currentUser.getIdToken(true);
}
// Attach the token to the request
config.headers.Authorization = `Bearer ${token}`;
return config;
});
return () => axios.interceptors.request.eject(authRequestInterceptor);
}, []);
Note: You should use the same useEffect() strategy for your response interceptor and you should use return firebase.auth().onAuthStateChanged(...) so that the cleanup function returned by onAuthStateChanged is accessible to its useEffect().
Here is my problem, in my react app whenever a order is created I want to get a Subscription for that order called orderNotification,
setup in order resolver:
Subscription: {
orderNotification: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(ORDER_NOTIFICATION)
}
}
mutation:
Mutation: {
async createOrder(_, { MakeOrderInput: { state, message, products, total } }, context) {
try {
const userAuth = isAuth(context);
const pubsub = context.pubsub;
const newOrder = new Order({
state,
username: userAuth.username,
user: userAuth.id,
createdAt: new Date().toISOString(),
total,
message,
products,
userAddress: userAuth.address,
});
const index = products.findIndex(x => x.cost === 0);
if (index != -1) {
const u = await User.findById({ _id: userAuth.id });
await User.findByIdAndUpdate({ _id: userAuth.id }, { points: u.points - 20 }, (err, data) => {
if (err) {
console.log(err)
} else {
console.log('fatto')
}
});
}
const order = await newOrder.save();
pubsub.publish(ORDER_NOTIFICATION, {
orderNotification: order
});
return order;
} catch (err) {
// throw new Error(err);
console.log(err)
}
},
all works fine in graphql Playground but when I have to get and show the results in my component the returned data is null:
import React from 'react'
import gql from 'graphql-tag';
import { useSubscription } from '#apollo/client';
import { Box } from 'grommet'
function SubscriptionOrder() {
const { data, loading, error } = useSubscription(SUBSCRIPTION_USER_ORDER, {
onSubscriptionData: (d) => console.log(d),
onSubscriptionComplete: (da) => console.log(da)
});
// return null
// console.log(data)
return (
<>
<Box style={{ marginTop: '96px' }}>
{data && data.orderNotification ? (
<h1>hi: {data.orderNotification.username}</h1>
) : (
<h1>NO DATA</h1>
)
}
</Box>
</>
)
};
const SUBSCRIPTION_USER_ORDER = gql`
subscription orderNotification{
orderNotification {
username
}
}
`;
export default SubscriptionOrder;
so considering that in playground works the error may be in my ApolloClient links configuration:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from '#apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { ApolloLink, Observable, split } from 'apollo-link';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { TokenRefreshLink } from "apollo-link-token-refresh";
import jwtDecode from "jwt-decode";
import { getAccessToken, setAccessToken } from './accessToken';
import dotenv from 'dotenv/config.js'
const cache = new InMemoryCache({});
const httpLink = new HttpLink({
uri: process.env.NODE_ENV === 'development' ? `${process.env.REACT_APP_SERVER_DEV}/graphql` : `${process.env.REACT_APP_SERVER_PRODUCTION}/graphql`,
credentials: "include",
});
const wsLink = new WebSocketLink({
uri: process.env.NODE_ENV === 'development' ? `ws://${process.env.REACT_APP_SERVER_DEV_WS}/graphql` : `ws://${process.env.REACT_APP_SERVER_PRODUCTION_WS}/graphql`,
options: {
reconnect: true,
lazy: true,
inactivityTimeout: 1000,
},
connectionCallback: err => {
if (err) {
console.log('Error Connecting to Subscriptions Server', err);
}
}
});
const splitLink = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscriptions";
},
wsLink,
httpLink
);
const requestLink = new ApolloLink(
(operation, forward) =>
new Observable(observer => {
let handle;
Promise.resolve(operation)
.then(operation => {
const accessToken = getAccessToken();
if (accessToken) {
operation.setContext({
headers: {
authorization: `Bearer ${accessToken}`
},
fetchOptions: {
credentials: 'include'
}
});
}
})
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const client = new ApolloClient({
link: ApolloLink.from([
new TokenRefreshLink({
accessTokenField: "accessToken",
isTokenValidOrUndefined: () => {
const token = getAccessToken();
if (!token) {
return true;
}
try {
const { exp } = jwtDecode(token);
if (Date.now() >= exp * 1000) {
return false;
} else {
return true;
}
} catch {
return false;
}
},
fetchAccessToken: () => {
return fetch(process.env.NODE_ENV === 'development' ? `${process.env.REACT_APP_SERVER_DEV}/refresh_token` : `${process.env.REACT_APP_SERVER_PRODUCTION}/refresh_token`, {
method: "POST",
credentials: "include"
});
},
handleFetch: accessToken => {
setAccessToken(accessToken);
},
handleError: err => {
console.warn("Your refresh token is invalid. Try to relogin");
console.error(err);
}
}),
onError(({ graphQLErrors, networkError }) => {
console.log(graphQLErrors);
console.log(networkError);
}),
requestLink,
splitLink,
]),
cache,
connectToDevTools: true,
credentials: 'include',
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode >,
document.getElementById('root')
);
here is my server:
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req, res }) => ({ req, res, pubsub }),
introspection: true,
cors: corsOptions,
});
server.applyMiddleware({ app, cors: false });
const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);
httpServer.listen(PORT, () => {
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`)
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`)
})
resolve the payload in the subscription with
Subscription: {
orderNotification: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(ORDER_NOTIFICATION),
resolve: (payload) => {
return payload;
},
}
}
another way to remove null or unwanted entries is withFilter to remove intimation in case of unknown or unwanted events;
writing this answer as an opyion, but the answer above is better
const { withFilter } = require('graphql-subscriptions');
Subscription: {
orderNotification: {
subscribe: withFilter(
() => pubsub.asyncIterator('ORDER_NOTIFICATION'),
(payload, variables) => {
// add any condition here
return (payload && payload !== null);
},
),
},
}
I am trying to get data from a object named initialState and console.log it, but the object is coming back empty.. related files below thanks in advance!
basketReducer.js
import { ADD_PRODUCT_BASKET, GET_NUMBERS_BASKET } from '../actions/types'
const initialState = {
basketNumbers: 0,
cartCost: 0,
products: {
blackOne: {
name: "Black One",
price: 120,
numbers: 0,
inCart: false
},
blackThree: {
name: "Black Three",
price: 120,
numbers: 0,
inCart: false
},
blackFour: {
name: "Black Four",
price: 120,
numbers: 0,
inCart: false
}
}
}
export default (state = initialState, action) => {
switch(action.type) {
case ADD_PRODUCT_BASKET:
let addQuantity = { ...state.products[action.payload] }
console.log(addQuantity)
return {
...state,
basketNumbers: state.basketNumbers + 1
}
case GET_NUMBERS_BASKET:
return {
...state
}
default:
return state
}
}
addAction.js
import { ADD_PRODUCT_BASKET} from './types'
export const addBasket = (productName) => {
return (dispatch) => {
console.log('Adding to Basket')
console.log('Product:', productName)
dispatch({
type: ADD_PRODUCT_BASKET,
payload: productName
})
}
}
types.js
export const ADD_PRODUCT_BASKET = 'ADD_PRODUCT_BASKET'
export const GET_NUMBERS_BASKET = 'GET_NUMBERS_BASKET'
FilterPanel.js
Note: I'm not focused on data from the state. For now I'm only using the onClick method in the render method to get this.props.addBasket(item.name)
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { addBasket } from '../actions/addAction'
import axios from 'axios'
import './FilterPanel.css'
export class FilterPanel extends Component {
constructor(props) {
super(props)
this.state = {
color: "",
shoes: []
}
this.handleColorChange = this.handleColorChange.bind(this)
}
componentDidMount() {
axios.get('http://localhost:4000/shoes')
.then(res => {
const shoeData = res.data
this.setState({
shoes: shoeData
})
})
.catch((error) => {
console.log(error)
})
}
handleColorChange(e) {
const color = e.target.value
console.log(color)
this.setState({
color: color
})
}
render() {
console.log(this.props)
const { shoes } = this.state
const colors = ["Black", "Blue", "Brown", "Gray", "Green", "Red", "White"]
const shoesToShow = shoes.map(item => {
if (item.mainColor === this.state.color) {
return <div key={item._id} className="each-shoe-div col-sm-6 col-md-6 col-lg-3">
<div>
<img className="img-fluid image" src={item.image} alt="pic"/>
<p>{item.name}</p>
<p>${item.price}</p>
<p onClick={ () => this.props.addBasket(item.name)} className="basket-p" >Add to cart</p>
</div>
</div>
} else {
return null
}
})
console.log(this.props)
return (
<React.Fragment>
<div className="form-div">
<form>
{colors.map((color, index) => (
<label key={index}>
{color}
<input
value={color}
checked={this.state.color === color}
onChange={this.handleColorChange}
type="radio"
/>
</label>
))}
</form>
</div>
<div className="filtered-shoes">{shoesToShow}</div>
</React.Fragment>
)
}
}
export default connect(null, { addBasket })(FilterPanel)
I would like the button click to lead to the cart page with the data info Added.
Here is the Real Data I want to use for my add to cart button:
shoe.model.js (Shoe schema):
const mongoose = require('mongoose')
const Schema = mongoose.Schema
let Shoe = new Schema({
image: {
type: String
},
cartImage: {
type: String
},
name: {
type: String
},
color: {
type: String
},
price: {
type: Number
},
mainColor: {
type: String
},
midImage: {
type: String
},
heroFeatured: {
type: Boolean
},
message: {
type: String
}
})
module.exports = mongoose.model('Shoe', Shoe)
shoe.route.js (shoe route):
const express = require('express')
const shoeRouter = express.Router()
const Shoe = require('./shoe.model')
shoeRouter.route('/shoes').get((req, res) => {
Shoe.find((err, shoes) => {
if(err) {
console.log(err)
} else {
res.json(shoes)
}
})
})
shoeRouter.route('/shoes/:id').get((req, res) => {
let id = req.params.id
Shoe.findById(id, (err, shoe) => {
res.json(shoe)
})
})
module.exports = shoeRouter
server.js (database connection / routes)
const express = require('express')
const app = express()
const cors = require('cors')
const mongoose = require('mongoose')
const shoeRouter = require('./shoe.route')
const userRouter = require('./user.route')
const emailRouter = require('./email.route')
//const routes = express.Router()
const PORT = 4000
app.use(cors())
app.use(express.urlencoded({ extended: false }))
app.use(express.json())
mongoose.connect('mongodb connection...', { useUnifiedTopology: true, useNewUrlParser: true })
const connection = mongoose.connection
connection.once('open', () => {
console.log("MongoDB database connection established successfully")
})
app.use('/', shoeRouter)
app.use('/', userRouter)
app.use('/', emailRouter)
app.listen(PORT, () => {
console.log('Server is running on port ' + PORT);
})
I didn't know how to implement a shopping cart so I followed a tutorial.
The initialState object from(basketReducer.js) is fake data i modeled after my real data.
I tried to add the fake data model from a tutorial (just added a copy of the real data)
How can I achieve this with my original data from (filterPanel.js state)?? Thanks!
I think this mapDispatchToProps is missing from your FilterPanel component.
Please try adding this -:
// old code from component
<div className="filtered-shoes">{shoesToShow}</div>
</React.Fragment>
)
}
}
function mapDispatchToProps(dispatch){
return {
addBasket: (param) => dispatch(addBasket(param))
}
}
export default connect(null, mapDispatchToProps)(FilterPanel)
I'm building a simple todo app using React, Apollo and react-apollo-hooks for hooks support, but the useSubscription hook doesnt fire.
I know the actual backend stuff works, because I have a graphiql app set up, and whenever I save a todo, the todoCreated event shows up in graphiql. I also know that the websocket-setup is working properly, because the queries & mutations are going through the websocket. I'm using Elixir, Phoenix, Absinthe, by the way, for the backend stuff.
Here's the Todo-app component:
import React, { useState } from 'react';
import gql from 'graphql-tag';
import { useQuery, useMutation, useSubscription } from 'react-apollo-hooks';
import styles from 'styles.css';
const TODO_FRAGMENT = gql`
fragment TodoFields on Todo {
id
description
}
`;
const GET_TODOS = gql`
{
todos {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const SAVE_TODO = gql`
mutation createTodo($description: String!) {
createTodo(description: $description) {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const DELETE_TODO = gql`
mutation deleteTodo($id: ID!) {
deleteTodo(id: $id) {
id
}
}
`;
const NEW_TODO_SUBSCRIPTION = gql`
subscription {
todoCreated {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const Todos = () => {
const [inputValue, setInputValue] = useState('');
const { data, error, loading } = useQuery(GET_TODOS);
const saveTodo = useMutation(SAVE_TODO, {
update: (proxy, mutationResult) => {
proxy.writeQuery({
query: GET_TODOS,
data: { todos: data.todos.concat([mutationResult.data.createTodo]) },
});
},
});
const deleteTodo = useMutation(DELETE_TODO, {
update: (proxy, mutationResult) => {
const id = mutationResult.data.deleteTodo.id
proxy.writeQuery({
query: GET_TODOS,
data: { todos: data.todos.filter(item => item.id !== id) },
});
},
});
const subData = useSubscription(NEW_TODO_SUBSCRIPTION);
console.log(subData);
if (loading) {
return <div>Loading...</div>;
};
if (error) {
return <div>Error! {error.message}</div>;
};
return (
<>
<h1>Todos</h1>
{data.todos.map((item) => (
<div key={item.id} className={styles.item}>
<button onClick={() => {
deleteTodo({
variables: {
id: item.id,
},
});
}}>Delete</button>
{' '}
{item.description}
</div>
))}
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
type="text"
/>
<button onClick={() => {
saveTodo({
variables: {
description: inputValue,
},
});
setInputValue('');
}}>Save</button>
</>
);
};
export default Todos;
And here's the root component:
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
import Todos from 'components/Todos';
import apolloClient from 'config/apolloClient';
const App = () => (
<ApolloHooksProvider client={apolloClient}>
<Todos />
</ApolloHooksProvider>
);
export default App;
Anyone have a clue on what I seem to be doing wrong?
Sorry, I figured it out, it was a silly mistake on my part. The problem seems to have been with my apolloClient setup:
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { ApolloLink } from 'apollo-link';
import absintheSocketLink from 'config/absintheSocketLink';
const apolloClient = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
new HttpLink({
uri: 'http://localhost:4000/api/graphql',
credentials: 'same-origin'
}),
absintheSocketLink,
),
]),
cache: new InMemoryCache()
});
export default apolloClient;
The error in the code above is the fact that the line
absintheSocketLink,
is in the wrong place. It should've been before the HttpLink.
Silly me.
I had the same issue my subscription was always sending null data and i had a silly mistake as well.
So to be short , the take effect of redux saga returns object which rpoperies are undefined except the type . Here is my saga `
import axios from 'axios';
import {
take, put, call
} from 'redux-saga/effects';
import {
getEventInfo, GET_EVENT_INFO
} from '../actions';
export default function* eventInfoSaga() {
try {
const { token, query } = yield take(GET_EVENT_INFO);
console.log(token, 'token'); // undefined
console.log(query, 'query'); // undefined
const options = {
method: 'post',
url: `https://www.eventbriteapi.com/v3/events/search/?q=${query}&expand=venue`,
credentials: 'include',
headers: { Authorization: `Bearer ${token}` }
};
const response = yield call(axios, options);
console.log(response, 'response eventInfoSaga');
yield put(getEventInfo(response));
} catch (err) {
console.log(err);
}
}
This is makeActionCreator`
const makeActionCreator = (type, ...argNames) => (...args) => {
const action = { type };
argNames.forEach((arg, index) => {
action[arg] = args[index];
});
console.log(action, 'actioooooooooooon');
return action;
};
export default makeActionCreator;
which i call with this `
import { makeActionCreator } from '../utilities';
export const GET_EVENT_INFO = 'GET_EVENT_INFO';
export const getEventInfo = makeActionCreator(GET_EVENT_INFO, 'token', 'query');
And this is the component where i dispatch an action with parameters `
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import SearchIcon from '../SvgIcons';
import MapComponent from '../Map';
import { getEventInfo, getUserInfo } from '../../actions';
class DashboardPage extends Component {
componentDidMount() {
const { dispatchUserInfo } = this.props;
dispatchUserInfo();
}
handleEventSearch = (e) => {
e.preventDefault();
const { dispatchEventInfo } = this.props;
const query = e.target.children[0].value;
console.log(query, 'queryyyyyyyy');
**dispatchEventInfo(query, query);**
}
render() {
console.log(this.props, 'proooops');
return (
<div className="dashboard-container">
<div className="search-event">
<form className="search-event__form" onSubmit={this.handleEventSearch}>
<input
autoComplete="off"
type="text"
name="search-event"
placeholder="Search an event"
className="search-event__input"
aria-label="Enter search text"
onChange={this.handleInputChange}
/>
<button type="submit" className="search-event__button">
<SearchIcon />
Search
</button>
</form>
<p className="sign-out">
Sign out
</p>
</div>
<div className="google-map">
<MapComponent
isMarkerShown
googleMapURL="https://maps.googleapis.com/maps/api/js?key=AIzaSyAGCyELoQaEHdu5GWT5WPTYU-T811MA4SY&v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: '100%' }} />}
containerElement={<div style={{ height: '100%' }} />}
mapElement={<div style={{ height: '100%' }} />}
/>
</div>
</div>
);
}
}
DashboardPage.defaultProps = {
dispatchUserInfo: null,
dispatchEventInfo: null,
};
DashboardPage.propTypes = {
dispatchUserInfo: PropTypes.func,
dispatchEventInfo: PropTypes.func
};
const mapStateToProps = (state) => {
const name = state.userReducer.name || '';
const accessToken = state.userReducer.accessToken || '';
return {
name,
accessToken
};
};
const mapDispatchToProps = dispatch => ({
dispatchEventInfo() {
dispatch(getEventInfo());
},
dispatchUserInfo() {
dispatch(getUserInfo());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
In redux-saga documentation they say that take(pattern) Creates an Effect description that instructs the middleware to wait for a specified action on the Store. The Generator is suspended until an action that matches pattern is dispatched.As i understand yield will make it wait that specific action and then do something (request, change, etc.).
So why i am getting undefined ? May be i misunderstand something