I am using Relay and react-router-relay and am trying to compose several Relay containers. The inner container needs a query fragment variable that must be passed from the router down through its parent containers. It's not getting this variable.
Here's how the code looks:
// A "viewer":
const UserQueries = { user: () => Relay.QL`query { user }` };
// A route to compose a message:
<Route
path='/compose/:id'
component={ ComposeContainer }
queries={ UserQueries }
/>
// A container:
const ComposeContainer = Relay.createContainer(
Compose,
{
initialVariables: {
id: null
},
fragments: {
user: () => Relay.QL`
fragment on User {
${ BodyContainer.getFragment('user') }
// other fragments
}
`
}
}
);
// And the composed container:
const BodyContainer = React.createContainer(
Body,
{
initialVariables: {
id: null
},
fragments: {
user: () => Relay.QL`
fragment on User {
draft(id: $id) {
// fields
}
}
`
}
}
);
The draft field inside BodyContainer never gets $id from the route param. The signature to RelayContainer.getFragment() has arguments that seem to let you pass params and variables, but I am not sure how this should be used.
Your user fragment on <ComposeContainer> has to be something like:
user: ({ id }) => Relay.QL`
fragment on User {
${BodyContainer.getFragment('user', { id })}
// other fragments
}
`
Additionally, when you compose in <BodyContainer> from <ComposeContainer>. You'll also need to pass in id as a prop, e.g.
<BodyContainer id={this.props.id} />
See also additional discussion at https://github.com/facebook/relay/issues/309.
Related
I am trying to use relay style pagination. However, I am getting trouble on infinite scrolling. When i scroll or load next sets of data then I just get the current data without it being merged to the previous data. This is how I have done
cache.ts
import { InMemoryCache } from '#apollo/client';
import { relayStylePagination } from '#apollo/client/utilities';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
conversation: relayStylePagination(),
},
},
},
});
export default cache;
Conversation Query
In my case, params like first, after, before, last are inside params object
export const CONVERSATION = gql`
query conversation($channel: ShortId, $contact: ShortId, $params: ConnectionInput) {
conversation(channel: $channel, contact: $contact, params: $params) {
status
data {
pageInfo {
...PageInfo
}
edges {
cursor
node {
...Communication
}
}
}
}
}
${PAGE_INFO}
${COMMUNICATION}
`;
Conversation.tsx
const [loadConversation, { data, fetchMore, networkStatus, subscribeToMore }] = useLazyQuery(
CONVERSATION,
);
useEffect(() => {
isMounted.current = true;
if (channelId && contactId) {
loadConversation({
variables: {
channel: channelId,
contact: contactId,
params: { first },
},
});
}
return () => {
isMounted.current = false;
};
}, [channelId, contactId, loadConversation]);
<React.Suspense fallback={<Spinner />}>
<MessageList messages={messages ? generateChatMessages(messages) : []} />
{hasNextPage && (
<>
<button
type='button'
ref={setButtonRef}
id='buttonLoadMore'
disabled={isRefetching}
onClick={() => {
if (fetchMore) {
fetchMore({
variables: {
params: {
first,
after: data?.conversation?.data?.pageInfo.endCursor,
},
},
});
}
}}
/>
</>
)}
</React.Suspense>
Can I know what I have missed?
The first, after, before, last should be declared as arguments of conversation rather than as properties of params.
Apollo merges the previous pages when the query arguments contain after/before .
query conversation($channel: ShortId, $contact: ShortId, $after: String, $first: Int, $before: String, $last: Int) {
conversation(channel: $channel, contact: $contact, after: $after, first: $first, before: $before, last: $last) {
...
}
}
I'm learning Gatsby and I wanted to use MDX for blog pages. I followed the tutorial here to programmatically create pages.
I can see them in my GraphQL, and displaying the list of all the articles is working with their title and slug, but when I click on the link to open them or type their address I have a 404 page. Do you have an idea from where it can come from?
This is my code:
gatsby-node.js:
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === "Mdx") {
const value = createFilePath({ node, getNode })
createNodeField({
name: "slug",
node,
value: `/blog${value}`,
})
}
}
gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-mdx`,
options: {
defaultLayouts: { default: path.resolve('./src/layouts/post.js') },
extensions: [`.mdx`, `.md`],
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `posts`,
path: `${__dirname}/src/posts`
}
},
],
index.js (with the list):
<ul>
{posts.map(({ node: post }) => (
<li key={post.id}>
<Link to={post.fields.slug}>
<h2>{post.frontmatter.title}</h2>
</Link>
<p>{post.excerpt}</p>
</li>
))}
</ul>
Query in index.js
export const pageQuery = graphql`
query blogIndex {
allMdx {
edges {
node {
id
excerpt
frontmatter {
title
}
fields {
slug
}
}
}
}
}
`
The template:
<div>
<div className="content" dangerouslySetInnerHTML={{ __html: post.html }}></div>
</div>
And the query for the template:
export const pageQuery = graphql`
query BlogPostQuery($id: String) {
mdx(id: { eq: $id }) {
id
body
frontmatter {
title
}
}
}
`
I made a repo if you want to test it: https://github.com/JulSeb42/gatsby-mdx
Thanks for your answers!
You are missing the page creation part. In your gatsby-node.js add the following:
const path = require("path")
exports.createPages = async ({ graphql, actions, reporter }) => {
// Destructure the createPage function from the actions object
const { createPage } = actions
const result = await graphql(`
query {
allMdx {
edges {
node {
id
fields {
slug
}
}
}
}
}
`)
if (result.errors) {
reporter.panicOnBuild('🚨 ERROR: Loading "createPages" query')
}
// Create blog post pages.
const posts = result.data.allMdx.edges
// you'll call `createPage` for each result
posts.forEach(({ node }, index) => {
createPage({
// This is the slug you created before
// (or `node.frontmatter.slug`)
path: node.fields.slug,
// This component will wrap our MDX content
component: path.resolve(`./src/components/posts-page-layout.js`),
// You can use the values in this context in
// our page layout component
context: { id: node.id },
})
})
}
Regarding your other issue, it should be a separate question but, if a component is exported as default, you don't need the curly braces when importing it. In addition, you need to return a value.
I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).
const MoreCommentsQuery = gql`
query MoreComments($cursor: String) {
moreComments(cursor: $cursor) {
cursor
comments {
author
text
}
}
}
`;
const CommentsWithData = () => (
<Query query={CommentsQuery}>
{({ data: { comments, cursor }, loading, fetchMore }) => (
<Comments
entries={comments || []}
onLoadMore={() =>
fetchMore({
// note this is a different query than the one used in the
// Query component
query: MoreCommentsQuery,
variables: { cursor: cursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
const previousEntry = previousResult.entry;
const newComments = fetchMoreResult.moreComments.comments;
const newCursor = fetchMoreResult.moreComments.cursor;
return {
// By returning `cursor` here, we update the `fetchMore` function
// to the new cursor.
cursor: newCursor,
entry: {
// Put the new comments in the front of the list
comments: [...newComments, ...previousEntry.comments]
}
};
}
})
}
/>
)}
</Query>
);
This is from the documentation so i cant show the code for the comments query but when executing the CommentsQuery, where does this cursor come from, is it literally just returning the argument we passed in or does the cursor field have to exist on MoreComments Query?
I'm running into an issue when I attempt to commit a deletion mutation. When I commit, I get the error Uncaught Invariant Violation: RelayMutationQuery: Invalid field name on fat query, `company`.. Viewing, creating and updating nodes all work. For some reason I just can't delete. It mentions the company field in the fatQuery, but the only field I have in the fat query is the deletedUserId I get back from the server. Thanks in advance!
Component:
import React, {Component} from 'react';
import Relay from 'react-relay';
import {Link} from 'react-router';
import DeleteUserMutation from 'mutations/DeleteUserMutation';
import styles from './EmployeeItem.css';
class EmployeeItem extends Component {
render() {
const {user} = this.props;
return (
<div className={styles.employee}>
<p><strong>ID:</strong> {user.id}</p>
<p><strong>First Name:</strong> {user.firstName}</p>
<p><strong>Last Name:</strong> {user.lastName}</p>
<p><strong>Email:</strong> {user.email}</p>
<div className="btn-group">
<Link to={`/company/employees/${user.id}`} className="btn btn-primary">View Employee</Link>
<button onClick={this.handleRemove} className="btn btn-danger">Delete User</button>
</div>
</div>
)
}
handleRemove = (e) => {
e.preventDefault();
const {user, company} = this.props;
Relay.Store.commitUpdate(new DeleteUserMutation({user, company}));
};
}
export default Relay.createContainer(EmployeeItem, {
fragments: {
company: () => Relay.QL`
fragment on Company {
id
${DeleteUserMutation.getFragment('company')}
}
`,
user: () => Relay.QL`
fragment on User {
id
firstName
lastName
email
${DeleteUserMutation.getFragment('user')}
}
`
}
});
Mutation:
import React from 'react';
import Relay from 'react-relay';
export default class DeleteUserMutation extends Relay.Mutation {
static fragments = {
company: () => Relay.QL`
fragment on Company {
id
}
`,
user: () => Relay.QL`
fragment on User {
id
}
`
};
getMutation() {
return Relay.QL`mutation {deleteUser}`;
}
getFatQuery() {
return Relay.QL`
fragment on DeleteUserPayload {
deletedUserId
}
`;
}
getVariables() {
return {
id: this.props.user.id,
}
}
getConfigs() {
return [{
type: 'NODE_DELETE',
parentName: 'company',
parentID: this.props.company.id,
connectionName: 'employees',
deletedIDFieldName: 'deletedUserId'
}]
}
// Wasn't sure if this was causing the error but it appears to be
// something else.
// getOptimisticResponse() {
// return {
// deletedUserId: this.props.user.id
// }
// }
}
This error is referring to the fact that you reference the "company" in your getConfigs() implementation. The NODE_DELETE config tells Relay how to construct the mutation query by mapping nodes in the store (e.g. parentID) to fields on the fat query (e.g. parentName).
Although you might not necessarily need it today, you should add the company to the mutation payload & fat query here, since the company is being affected by this change. More specifically, the company's employees connection is being modified :)
NevilleS' solution solved it for me:
I added a globalId to the root field (in my case an object called "verify") and I also changed my mutation on the server to return an edge, rather than just the underlying type. I also added the root "verify" object to the mutation output fields: it would make sense that the client's relay mutation needs that to know which object owns the connection, where to put the new edge.
export const Verify = new GraphQLObjectType({
name: 'Verify',
fields: () => ({
id: globalIdField('Verify'),
verifications: {
args: connectionArgs,
type: VerificationConnection,
resolve: (rootValue, args) => connectionFromArray(rootValue.verifications, args)
},
Adding "verify" and "verificationEdge" to the mutation's output fields.
export const AddVerifiedSchool = mutationWithClientMutationId({
name: 'AddVerifiedSchool',
inputFields: {
verification: {
type: VerifiedSchoolInput
}
},
outputFields: {
success: {
type: GraphQLBoolean,
resolve: () => true
},
verificationEdge: {
type: VerificationEdge,
resolve: ({verification, context}) => {
console.log('verification', verification);
return verification
}
},
verify: {
type: Verify,
resolve: ({verification, context}) => {
return context.rootValue
}
}
},
Adding the verify field to the fat query, and (the globalId "id" from verify) to the fragments, and using the new globalId to identify the node where the connection exists.
static fragments = {
verify: () => Relay.QL`fragment on Verify { id }`,
action: () => Relay.QL`fragment on Action { name url }`
};
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'verify',
parentID: this.props.verify.id,
connectionName: 'verifications',
edgeName: 'verificationEdge',
rangeBehaviors: {
'': 'append'
}
}];
}
getFatQuery() {
return Relay.QL`
fragment on AddVerifiedSchoolPayload {
verification {
${VerifiedSchool.getFragment('verification')}
}
verify {
id
}
}`
}