Closed nealoke closed 4 years ago
Let's hope I don't get multiple bugs this time, I'm also waiting on a breaking change in Next.js for the Apollo implementations, then I guess I'll do another major.
It is actually working fine π , the only issue is when the react-apollo
package is not installed. A fix would be nice as apollo is now splitting into multiple packages for lighter weight?
As soon as I upgraded react-apollo
to 3.0.0
I started getting the following error:
Invariant Violation: Could not find "client" in the context or passed in as a prop. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via props.
Assuming it might be related? Rolling back react-apollo
to ^2.5.8
and the error went away
@mikestecker don't forget to use the ApolloProvider
from the @apollo/react-hooks
in stead of the react-apollo
one.
@nealoke Thanks, I've tried that but I started to get even more errors.
In the browser I'm seeing:
Could not find "client" in the context or passed in as a prop. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via props.
And in my terminal I'm getting Error: Not allowed by CORS
errors now like crazy which I've never had before trying to implement hooks for some strange reason. My backend is using GraphQL-Yoga and Prisma if that matters.
Also, looks like I have to go back and migrate all my compose
code too
@mikestecker, I am very new to both Next.js and Apollo/GraphQL, but I've got useQuery
working with SSR and everything. This might be a too-simple example, but maybe it will help you out:
// helpers/withApollo.ts
import withApollo from 'next-with-apollo'
import ApolloClient, { InMemoryCache } from 'apollo-boost'
const GRAPHQL_URL = 'https://dog-graphql-api.glitch.me/graphql'
export default withApollo(
({ initialState }) =>
new ApolloClient({
cache: new InMemoryCache().restore(initialState || {}),
uri: GRAPHQL_URL,
})
)
// pages/_app.tsx
import { ApolloClient } from 'apollo-boost'
import { default as NextApp, createUrl } from 'next/app'
import { Router } from 'next/router'
import {
loadGetInitialProps,
AppContextType,
AppInitialProps,
} from 'next-server/dist/lib/utils'
import { ApolloProvider } from '@apollo/react-hooks'
import Container from 'components/Container'
import withApollo from 'helpers/withApollo'
interface Props {
apollo: ApolloClient<{}>
}
class App extends NextApp<Props> {
static async getInitialProps({
Component,
ctx,
}: AppContextType<Router>): Promise<AppInitialProps> {
let pageProps = await loadGetInitialProps(Component, ctx)
return { pageProps }
}
render() {
const url = createUrl(this.props.router)
return (
<ApolloProvider client={this.props.apollo}>
<Container
Component={this.props.Component}
pageProps={this.props.pageProps}
url={url}
/>
</ApolloProvider>
)
}
}
export default withApollo(App)
// pages/index.tsx
import { useQuery } from '@apollo/react-hooks'
import gql from 'graphql-tag'
import Head from 'next/head'
export const dogsQuery = gql`
{
dogs {
id
breed
}
}
`
const Index = () => {
const { data, error, loading } = useQuery(dogsQuery)
if (error) {
return <div>Error</div>
}
if (loading) {
return <div>Loading</div>
}
return (
<>
<Head>
<title>Dogs</title>
</Head>
<ul>
{data.dogs.map((dog: any, i: number) => (
<li key={dog.id}>
<div>
<span>{i + 1}. </span>
<span>{dog.breed}</span>
</div>
</li>
))}
</ul>
</>
)
}
export default Index
// package.json
"dependencies": {
"apollo-boost": "^0.4.3",
"graphql": "^14.4.2",
"next": "^9.0.0",
"next-with-apollo": "^4.2.0",
"react": "^16.8.6",
"react-apollo": "^3.0.0",
"react-dom": "^16.8.6"
},
I'm glad I started using this just as hooks support came out, because the syntax is so much nicer than using the Query
component.
@markplindsay thanks for the code sample, I don't know what it is but I just disabled my "whitelist" approach to CORS which solved the issue. Something with the changes to react-apollo
was causing an origin
mismatch somehow, somewhere. No idea. I don't know enough about CORS to figure it out.
I had this on my server side:
const whitelist = [process.env.FRONTEND_URL, process.env.ADMIN_URL];
server.express.use(
"/*",
cors({
credentials: true,
origin: function(origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
}
})
);
Which was having issues. I changed that entire origin
function block to simply be origin: true
and all my errors went away.
Apologies for hijacking the comment thread here with what turned out to be an unrelated problem!
@markplindsay Congrats on making it work, I'll update the package once a breaking change in Next.js is released, then I'll do a major updating the package and removing react-apollo
too
@mikestecker Have a look at the docs for the cors origin
. Basically if you want to allow everything just set it to origin: true
. If a request comes in with origin test.com
it will respond with allowed origins: test.com
.
Hey, guys, I am just trying to get this working. I have set up this as described in the readme but my application fails with the invalid hooks error message. My react version matches the dom version.
yarn list v1.17.3
ββ @apollo/react-common@3.0.0
ββ @apollo/react-components@3.0.0
ββ @apollo/react-hoc@3.0.0
ββ @apollo/react-hooks@3.0.0
ββ @apollo/react-ssr@3.0.0
ββ @babel/helper-builder-react-jsx@7.3.0
ββ @babel/plugin-transform-react-display-name@7.2.0
ββ @babel/plugin-transform-react-jsx-self@7.2.0
ββ @babel/plugin-transform-react-jsx-source@7.5.0
ββ @babel/plugin-transform-react-jsx@7.3.0
ββ @babel/preset-react@7.0.0
ββ @semantic-ui-react/event-stack@3.1.0
ββ @types/react-dom@16.8.5
ββ @types/react@16.9.1
ββ babel-plugin-react-require@3.0.0
ββ babel-plugin-transform-react-remove-prop-types@0.4.24
ββ create-react-context@0.2.3
ββ hoist-non-react-statics@3.3.0
ββ next-server@9.0.3
β ββ react-is@16.8.6
ββ next@9.0.3
β ββ react-is@16.8.6
ββ react-apollo@3.0.0
ββ react-dom@16.9.0
ββ react-error-overlay@5.1.6
ββ react-fast-compare@2.0.4
ββ react-is@16.9.0
ββ react-popper@1.3.3
β ββ create-react-context@0.2.2
ββ react@16.9.0
ββ semantic-ui-react@0.87.3
This is my LoginForm :
import * as React from 'react';
import * as Yup from 'yup';
import { Formik, FormikActions } from 'formik';
import { Form, Button } from 'semantic-ui-react';
import { gql } from 'apollo-boost';
import { useMutation } from '@apollo/react-hooks';
const LoginFormSchema = Yup.object().shape({
username: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Username is required !'),
password: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Password is required !')
});
const DO_LOGIN = gql`
mutation DoLogin($cred: LoginCredentialsInput!) {
login(credentials: $cred)
}
`;
interface Props {
loginMethod(values: any, actions: FormikActions<any>): void;
}
export const LoginForm: React.FunctionComponent<Props> = ({ loginMethod }) => (
<div>
<h1>Login</h1>
<Formik
initialValues={{
username: '',
password: ''
}}
validationSchema={LoginFormSchema}
onSubmit={(values, actions: FormikActions<any>) => {
const [login, { data }] = useMutation(DO_LOGIN);
login({ variables: { cred: values } });
console.log(data);
loginMethod(values, actions);
}}
>
{({ errors, touched, handleChange, handleSubmit, isSubmitting }) => (
<Form
size='small'
onSubmit={handleSubmit}
loading={isSubmitting}
widths='equal'
>
<Form.Input
name='username'
onChange={handleChange}
error={
errors.username && touched.username
? {
content: errors.username,
pointing: 'below'
}
: null
}
icon='user'
iconPosition='left'
placeholder='Username'
fluid
autoFocus
/>
<Form.Input
name='password'
type='password'
onChange={handleChange}
error={
errors.password && touched.password
? {
content: errors.password,
pointing: 'below'
}
: null
}
icon='lock'
iconPosition='left'
placeholder='Password'
fluid
/>
<Button type='submit'>Submit</Button>
</Form>
)}
</Formik>
</div>
);
Is there something obvious I am doing wrong quite new to react, apollo and would appreciate any hints. The source code is also available at https://github.com/S0PEX/Content-Web-App Regards Artur
@S0PEX You cant use a hook inside an event handler(onSubmit). You should move the useMutation hook to the top of your component.
@Corjen Thanks that did the trick so do I always use the mutation and query from the react hooks or from the apollo library or is there no difference? Because somebody mentioned that you should use react hooks over apollo react.
@mikestecker Have a look at the docs for the cors
origin
. Basically if you want to allow everything just set it toorigin: true
. If a request comes in with origintest.com
it will respond withallowed origins: test.com
.
@nealoke I double checked everything and ran some logging on the origin
and it's coming back as undefined
on the some hits to the server and then it would come through correctly on others. Seems entirely inconsistent. Probably an issue on my frontend code with Apollo?
Screenshot of my console.log
:
I logged both the origin
and my whitelist that it should be checking against.
Here's my frontend code:
./pages/_app.js:
import App, { Container } from "next/app";
import Root from "../components/Root";
import { ApolloProvider } from "@apollo/react-hooks";
import withData from "../lib/withData";
class MyApp extends App {
// gets all page properties and queries before loading to pass along
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
render() {
const { Component, apollo, pageProps } = this.props;
return (
<Container>
<ApolloProvider client={apollo}>
<Root client={apollo}>
<Component client={apollo} {...pageProps} />
</Root>
</ApolloProvider>
</Container>
);
}
}
export default withData(MyApp);
./lib/withData.js:
import withApollo from "next-with-apollo";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import * as ws from "ws";
import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import { onError } from "apollo-link-error";
import { ApolloLink, Observable, split } from "apollo-link";
import { RetryLink } from "apollo-link-retry";
import { createUploadLink } from "apollo-upload-client";
import { endpoint, wsEndpoint } from "../config";
const request = (operation, headers) => {
operation.setContext({
fetchOptions: {
credentials: "include"
},
headers
});
};
function createClient({ ctx, headers, initialState }) {
const httpLink = new HttpLink({
uri: process.env.NODE_ENV === "development" ? endpoint : endpoint
});
const wsLink = process.browser
? new WebSocketLink({
uri: process.env.NODE_ENV === "development" ? wsEndpoint : wsEndpoint,
options: {
reconnect: true
}
})
: () => {
console.log("SSR");
};
return 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}`);
if (networkError) console.log("[Network error]: ", networkError);
}),
new ApolloLink(
(operation, forward) =>
new Observable(observer => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper, headers))
.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();
};
})
),
new RetryLink().split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
// httpLink,
// httpLink breaks uploader
createUploadLink({
uri: process.env.NODE_ENV === "development" ? endpoint : endpoint
})
)
]),
cache: new InMemoryCache().restore(initialState || {})
});
}
export default withApollo(createClient);
and on the backend, here's the possible relevant code from graphql-yoga:
./createServer.js:
const { GraphQLServer, PubSub } = require("graphql-yoga");
const depthLimit = require("graphql-depth-limit");
const Mutation = require("./resolvers/Mutation");
const Query = require("./resolvers/Query");
const Subscription = require("./resolvers/Subscription");
const Conversation = require("./resolvers/Conversation");
const db = require("./db");
const pubsub = new PubSub();
// Create the GraphQL Yoga Server
function createServer() {
return new GraphQLServer({
typeDefs: "src/schema.graphql",
resolvers: {
Mutation,
Query,
Subscription,
Conversation
},
resolverValidationOptions: {
requireResolversForResolveType: false
},
// https://github.com/stems/graphql-depth-limit
// https://blog.apollographql.com/securing-your-graphql-api-from-malicious-queries-16130a324a6b
validationRules: [depthLimit(10)],
uploads: {
// Limits here should be stricter than config for surrounding
// infrastructure such as Nginx so errors can be handled elegantly by
// graphql-upload:
// https://github.com/jaydenseric/graphql-upload#type-uploadoptions
maxFileSize: 10000000, // 10 MB
maxFiles: 20
},
context: (req, connection) => ({
...req,
pubsub,
db
})
});
}
module.exports = createServer;
index.js:
require("dotenv").config({ path: ".env" });
const cookie = require("cookie");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const jwt = require("jsonwebtoken");
const expressip = require("express-ip");
const helmet = require("helmet");
const compression = require("compression");
const { S3 } = require("aws-sdk");
const logger = require("./utils/logger");
const createServer = require("./createServer");
const db = require("./db");
const server = createServer();
const s3client = new S3({
accessKeyId: process.env.S3_KEY,
secretAccessKey: process.env.S3_SECRET,
params: {
Bucket: process.env.S3_BUCKET
}
});
server.express.use(helmet());
server.express.use(compression());
server.express.use(cookieParser());
server.express.use(expressip().getIpInfoMiddleware);
// get the users IP information
// note: doesn't work with localhost
server.express.use((req, res, next) => {
const userIp = req.ipInfo;
if (userIp) {
req.userIp = userIp;
}
next();
});
// decode the JWT so we can ge the user ID on each request
server.express.use((req, res, next) => {
const { token } = req.cookies;
if (token) {
const { userId } = jwt.verify(token, process.env.APP_SECRET);
// put the userId onto the request for future requests to access
req.userId = userId;
}
next();
});
// create a middleware that populates the user on each request
server.express.use(async (req, res, next) => {
// if they aren't logged in, skip this
if (!req.userId) return next();
const user = await db.query.user(
{ where: { id: req.userId } },
"{ id, email, emailMask, emailVerified, role, permissions, account { accountType } }"
);
req.user = user;
next();
});
const whitelist = [process.env.FRONTEND_URL, process.env.ADMIN_URL];
// TODO: FIGURE OUT CORS ISSUES
server.express.use(
"/*",
cors({
credentials: true,
origin: function(origin, callback) {
console.log("origin", origin);
console.log("whitelist", whitelist);
if (whitelist.indexOf(origin) !== -1) {
// callback(null, true);
} else {
// callback(new Error("Not allowed by CORS"));
}
// allow everything to pass for now while I figure out the issue
callback(null, true);
}
})
); // allow cors
// if (process.env.NODE_ENV === 'development') server.express.use(logger);
server.start(
{
// TODO: FIGURE OUT CORS ISSUES
cors: {
credentials: true,
origin: function(origin, callback) {
console.log("origin", origin);
console.log("whitelist", whitelist);
if (whitelist.indexOf(origin) !== -1) {
// callback(null, true);
} else {
// callback(new Error("Not allowed by CORS"));
}
// allow everything to pass for now while I figure out the issue
callback(null, true);
}
},
subscriptions: {
onConnect: async (connectionParams, webSocket) => {
console.log("Websocket CONNECTED");
const header = webSocket.upgradeReq.headers.cookie;
const { token } = cookie.parse(header);
try {
const promise = new Promise((resolve, reject) => {
const { userId } = jwt.verify(token, process.env.APP_SECRET);
resolve(userId);
});
const user = await promise;
return user;
} catch (err) {
throw new Error(err);
}
},
onDisconnect: () => console.log("Websocket DISCONNECTED")
}
},
deets => {
console.log(
`π Backend is now running on port http:/localhost:${deets.port}`
);
}
);
I've created #77 which uses @apollo/react-ssr instead of react-apollo.
@mikestecker hmm weird, below is my setup as it might help.
"@apollo/react-hooks": "^3.0.0",
"apollo-boost": "^0.4.3",
"graphql": "^14.4.2",
"next": "^9.0.3",
"next-with-apollo": "^4.2.0",
"react": "^16.8.6",
"react-apollo": "^3.0.0",
"react-dom": "^16.8.6"
server.js
(back-end)import dotenv from "dotenv";
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import jwt from "jsonwebtoken";
import apolloServer from "./apolloServer";
dotenv.config();
export const createServer = () => {
/* ---------- Config ----------*/
const port = process.env.PORT || 7000;
const app = express();
app.disable("x-powered-by");
/* ---------- Middleware ----------*/
// CORS
const corsSettings = { credentials: true, origin: true };
app.use(cors(corsSettings));
// Body
app.use(bodyParser.json());
// Cookies
app.use(cookieParser());
/* ---------- Routes ----------*/
// app.use(...);
apolloServer.applyMiddleware({ path: "/graphql", cors: corsSettings, app });
// Listen
return app.listen(port, () => console.log(`API listening on port ${port}!`));
};
withData
HOC for integrating apollo
in next.js
import withApollo from "next-with-apollo";
import ApolloClient, { InMemoryCache } from "apollo-boost";
function createClient({ initialState, headers }) {
return new ApolloClient({
uri: "http://localhost:7000/graphql",
cache: new InMemoryCache().restore(initialState || {}),
request: (operation) => {
operation.setContext({
fetchOptions: {
credentials: "include"
},
headers
});
}
});
}
export default withApollo(createClient);
_app.js
import React from "react";
import App, { Container } from "next/app";
import { ApolloProvider } from "@apollo/react-hooks";
import withData from "../HOC/withData";
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, apollo, pageProps } = this.props;
return (
<Container>
<ApolloProvider client={apollo}>
<Component {...pageProps} />
</ApolloProvider>
</Container>
);
}
}
export default withData(MyApp);
@lfades any plans for a major release to support hooks again?
@adamsoffer I don't understand why people are getting the feeling that it is not supported at the moment. I thought this too but the comment above you (my setup) it works 100% with hooks and no issues or bugs π.
@nealoke hm Iβl try it, but thereβs no hook support in this package so Iβm perplexed how that might work. The author published hooks support than reverted because he wanted to wait for apollo hooks to come out of beta which it has been for quite a few weeks now.
@mikestecker hmm weird, below is my setup as it might help.
@nealoke Thanks. After some more digging I think I was able to get my issue resolved.
@adamsoffer Right, the hooks update wasn't working in my example projects, I've been reviewing the example packages in Next.js that use Apollo, I certainly believe that the implementation is becoming way easier and this package may no longer be required but just some lines of code (I'm in the Next.js team)
Also there are good code optimizations that can be done by just including the implementation, once I have a good insight of how it's going to look like, I'll update the package again and maybe recommend a custom implementation instead. getDataFromTree
is no longer working as before, e.g using it in the browser doesn't really works so the configs in my package for it are not very useful, apollo-boost
will be removed and the apollo client packages seem to be going through a refactor so maybe the implementation of Apollo (If my wishes become true) will be even better.
If you're using this package and need hooks, this is the implementation that I currently recommend: with-apollo/lib/apollo.js
@lfades funny I came to the same conclusion this week and stopped using a package altogether. Thanks!
@lfades Don't like that you need to wrap every page with that HOC instead of just App once with the implementation you're referencing. Edit: I just upgraded from Next.js 8 to 9 and I already see why they recommend wrapping each page, still it's not really convenient if you have a large application where a lot of pages need graphql imho.
@wouterds Because you shouldn't need to add Apollo to all pages unless they all should have it, and when your application grows you'll start finding exceptions
As of 2 hours ago official support for hooks by apollo has landed in
version 3.0.0.
. Would love to use this together with this HOC correctly. π