merge previous data while scrolling on relay pagination graphql - javascript

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) {
...
}
}

Related

Search in a table on multiple columns with React, Node.js and Mongo

There is a table with multiple columns and now it is possible to search for a column. When a text is written in the search box, a filter is performed and only the rows containing that word on that column are shown.
For example, there are 3 columns: name, brand, sales. The search is done for column name. I want to make it search through all the columns but I don't know how.
Here is how is works now.
React file:
import React from 'react';
import { connect } from 'react-redux';
import { Creators } from '../../../actions';
import Layout from '../../../components/Layout/Layout';
import ContentContainer from '../../../components/Layout/ContentContainer';
import GenericTable from '../../../components/Table/GenericTable';
import OneColumn from '../../../components/Layout/OneColumn';
import SearchBox from '../../../components/SearchBox/SearchBox.component';
import TabContainer from '../../../components/Layout/TabContainer';
class Products extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
query: {
name: '',
brand: '',
sales: '',
},
};
}
componentDidMount() {
const { getProducts, getProviders } = this.props;
getProducts(this.state.query);
getProviders();
}
searchHandler = event => { // here is the search function
const { getProducts } = this.props;
getProducts({
name: event.target.value, // it works for only for name column now
brand: this.props.query.brand,
sales: this.props.query.sales,
});
};
render() {
const listHeaders = ['ID', 'Name', 'Brand', 'Sales'];
const listTab = (
<ContentContainer>
<div>
<div>
<SearchBox
placeholder="Search product"
onChange={this.searchHandler} // here is the search box
/>
</div>
<GenericTable // the table
id="products-table"
headers={listHeaders}
rows={listRows}
entityName="products"
idList={listIdList}
/>
</div>
</ContentContainer>
);
return (
<Layout>
<OneColumn>
<TabContainer>
<Tab.Pane>{listTab}</Tab.Pane>
</TabContainer>
</OneColumn>
</Layout>
);
}
}
const mapStateToProps = state => ({
products: state.products.products,
query: state.products.query,
});
const mapDispatchToProps = {
getProducts: Creators.getProductsRequest,
};
export default connect(mapStateToProps, mapDispatchToProps)(Products);
The query is built with redux-saga:
const api = API.create();
export function* getProducts({ query }) {
let urlQuery = '';
if (query && query.name) {
urlQuery += `&name=${query.name}`;
}
if (query && query.brand) {
urlQuery += `&brand=${query.brand}`;
}
if (query && query.sales) {
urlQuery += `&price=${query.sales}`;
}
try {
const response = yield call(api.getProducts, urlQuery);
yield put(Creators.getProductsSuccess(response.data));
} catch (error) {
yield put(Creators.getProductsFailure(error));
}
}
In Node.js there are 3 methods for the 3 search options, maybe they need to be combined in one but don't know how:
findAll: (req, res) => {
const aggregateOptions = [];
// search by name
if (req.query.name) {
aggregateOptions.push({ $match: { name: { $regex: req.query.name, $options: 'i' } } });
}
// search by brand
if (req.query.brand) {
aggregateOptions.push({ $match: { brand: { $regex: req.query.brand, $options: 'i' } } });
}
// search by sales
if (req.query.sales) {
aggregateOptions.push({ $match: { brand: { $regex: req.query.sales, $options: 'i' } } });
}
aggregateOptions.push({
$project: {
productId: 1,
name: 1,
brand: 1,
sales: 1,
},
});
const myAggregate = Product.aggregate(aggregateOptions);
Product.aggregatePaginate(myAggregate, options)
.then(function(results) {
return res.send(results);
})
.catch(function(err) {
return res.status(400).send({ message: err.message });
});
}
So, when I write "s" in the search box, this is the API call: http://localhost:5000/products&name=s.
I don't know if it's possible to do something like http://localhost:5000/products&name=s || brand=s || sales=s or how can this be made to search from a single search box through all 3 columns.
Before calling getProducts in search Handler you have to call setState and update the state. Concerning what yoh asked I have a solution that require some refactoring, you can set the state to {query: ''} to only filter text and you send it to backend where you perform the searching in the database ( you search for the query filter text in the 1st column then in the 2nd then in the 3rd and push all the results to the same array.
EDITED for redux saga
const api = API.create();
export function* getProducts({ query }) {
let urlQuery = '';
if(query){
urlQuery += `&query=${query}`;
}
try {
const response = yield call(api.getProducts, urlQuery);
yield put(Creators.getProductsSuccess(response.data));
} catch (error) {
yield put(Creators.getProductsFailure(error));
}
}

how to setState an array which is nested inside an array of objects

this is my state
this.state = {
notification: [{
from: {
id: someid,
name: somename
},
message: [somemessage]
},
{..},
{..},
]
}
Now if i get a new message from someid, i have to push that new message into message array of someid
I tried to push that message in different ways, but nothing has worked
I tried it in this way, but im not able to push a new message into message array
if (this.state.notification) {
for (let q = 0; q < this.state.notification.length; q++) {
if (
this.state.notification[q] &&
this.state.notification[q].from &&
this.state.notification[q].from.id === r.from.id
) {
this.setState({
notification: [
...this.state.notification[q].messages,
this.state.notification[q].messages.push(r.message),
],
});
return console.log(this.state.notification[q].messages)
}
}
} else {
this.setState({
notification: [{
from: r.from,
messages: [r.message]
}, ]
});
return console.log(JSON.stringify(this.state.notification));
}
First of all, I think structuring your state as a 2d array is not a good idea. But you can try this
const pushMessage = (someId, someMessage) => {
this.setState({
notifications: this.state.notifications.map((notification) => {
if (notification.from.id === someId) {
return {
...notification,
messages: [...notification.messages, someMessage],
};
}
return notification;
}),
});
};
I'm pretty sure you can't do this: this.state.notification[q].messages.push(r.message). You can't mutate your state directly. You should work with a copy o your state, modify it with your code that seems to be ok, and then do the setState(...).
Here is a repro on Stackblitz that works. Here is the code :
import React, { Component } from "react";
import { render } from "react-dom";
import "./style.css";
class App extends Component {
constructor() {
super();
this.state = {
notifications: [
{
from: { id: 0, name: "Quentin" },
message: ["Message 1"]
},
{
from: { id: 1, name: "John" },
message: ["Message 1"]
},
{
from: { id: 2, name: "Henry" },
message: ["Message 1"]
}
]
};
this.pushMessage = this.pushMessage.bind(this);
}
pushMessage (id, message) {
const newState = Object.assign([], this.state);
newState.notifications.forEach(notif => {
if (notif.from.id === id) {
notif.message.push(message);
}
});
this.setState(newState, () => console.log(this.state));
};
render() {
return (
<div>
<button onClick={() => this.pushMessage(1, "Hello World")}>
Push message
</button>
</div>
);
}
}
render(<App />, document.getElementById("root"));
I only handle the push of a message in an existing notification, you already got the other case.
The first argument to setState is an updater function that takes the previous state as an argument. I think you should use this fact to update your state correctly.
Check out this answer https://medium.com/#baphemot/understanding-reactjs-setstate-a4640451865b.

Infinite scroll fetchmore

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?

Relay Error when deleting: RelayMutationQuery: Invalid field name on fat 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
}
}`
}

How can I compose query fragments with variables

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.

Categories

Resources