React Apollo first object from subscription not being merge into previous data it actually gets removed - reactjs

I have a query which gets me a list of notes and a subscription which listens and inserts new notes by altering the query. However the problem is the first note doesn't get added.
So let me add more detail, initially the query response with an object which contains an attribute called notes which is an array of 0 length, if we try and add a note the attribute gets removed. The note is created so if I refresh my application the query will return the note then If I try and add a note again the note gets added to the array in the query object.
Here is my notes container where I query for notes and create a new property to subscribe to more notes.
export const NotesDataContainer = component => graphql(NotesQuery,{
name: 'notes',
props: props => {
console.log(props); // props.notes.notes is undefined on first note added when none exists.
return {
...props,
subscribeToNewNotes: () => {
return props.notes.subscribeToMore({
document: NotesAddedSubscription,
updateQuery: (prevRes, { subscriptionData }) => {
if (!subscriptionData.data.noteAdded) return prevRes;
return update(prevRes, {
notes: { $unshift: [subscriptionData.data.noteAdded] }
});
},
})
}
}
}
})(component);
Any help would be great, thanks.
EDIT:
export const NotesQuery = gql`
query NotesQuery {
notes {
_id
title
desc
shared
favourited
}
}
`;
export const NotesAddedSubscription = gql`
subscription onNoteAdded {
noteAdded {
_id
title
desc
}
}
`;
Another EDIT
class NotesPageUI extends Component {
constructor(props) {
super(props);
this.newNotesSubscription = null;
}
componentWillMount() {
if (!this.newNotesSubscription) {
this.newNotesSubscription = this.props.subscribeToNewNotes();
}
}
render() {
return (
<div>
<NoteCreation onEnterRequest={this.props.createNote} />
<NotesList
notes={ this.props.notes.notes }
deleteNoteRequest={ id => this.props.deleteNote(id) }
favouriteNoteRequest={ this.props.favouriteNote }
/>
</div>
)
}
}
Another edit:
https://github.com/jakelacey2012/react-apollo-subscription-problem

YAY got it to work, simply the new data sent down the wire needs to be the same shape as the original query.
e.g.
NotesQuery had this shape...
query NotesQuery {
notes {
_id
title
desc
shared
favourited
}
}
yet the data coming down the wire on the subscription had this shape.
subscription onNoteAdded {
noteAdded {
_id
title
desc
}
}
notice shared & favourited are missing from the query on the subscription. If we added them it would now work.
This is the problem, react-apollo internally detects a difference and then doesn't add the data I guess It would be useful if there was a little more feed back.
I'm going to try and work with the react-apollo guys to see if we can put something like that in place.
https://github.com/apollographql/react-apollo/issues/649

Related

ApolloClient v3 fetchMore with nested query results

I'm using ApolloClient 3 the GitHub GraphQL API to retrieve all releases from a repo.
This is what the query looks like:
query ($owner: String!, $name: String!, $first: Int, $after: String, $before: String) {
repository(owner: $owner, name: $name) {
id
releases(orderBy: {field: CREATED_AT, direction: DESC}, first: $first, after: $after, before: $before) {
nodes {
name
publishedAt
resourcePath
tagName
url
id
isPrerelease
description
descriptionHTML
}
totalCount
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
}
This is what the result payload looks like:
This returns me the first x entries (nodes). So far, all good.
I need to implement pagination and I make use of the fetchMore function provided by ApolloClient useQuery. Calling fetchMore fetches the next x entries successfully but these are not displayed in my component list.
According to the ApolloClient Pagination documentation, it seems necessary to handle the merging of the fetchMore results with the ApolloClient caching mechanism. The documentation is understandable for simple situations but I am struggling to implement a solution for the situation where the actual array of results that needs to be merged togeher is deeply nested in the query result (repository -> releases -> nodes).
This is my implementation of the InMemoryCache options merge:
const inMemoryCacheOptions = {
addTypename: true,
typePolicies: {
ReleaseConnection: {
fields: {
nodes: {
merge(existing, incoming, options) {
const previous = existing || []
const results = [...previous, ...incoming]
return results
}
}
}
},
}
}
The results array here contains the full list, including the existing entries and the new x entries. This is essentially the correct result. However, my component list which is using the useQuery and fetchMore functionality does not get the new entries after the fetchMore is called.
I have tried various combinations in the inMemoryCacheOptions code above but so far I have been unsuccessful.
To add more context, this is the related component code:
export default function Releases() {
const { loading, error, data, fetchMore } = useQuery(releasesQuery, {
variables: {
owner: "theowner",
name: "myrepo",
first: 15
}
});
if (loading) return null;
if (error) {
console.error(error);
return null;
}
if (data) {
console.log(data?.repository?.releases?.pageInfo?.endCursor);
}
const handleFetchMore = () => {
fetchMore({
variables: {
first: 15,
after: data?.repository?.releases?.pageInfo?.endCursor
}
});
};
return (
<div>
<ul>
{data?.repository?.releases?.nodes?.map(release => (
<li key={release.id}>{release.name}</li>
))}
</ul>
<button onClick={handleFetchMore}>Fetch More</button>
</div>
);
}
After fetchMore the component doesn't rerender with the new data.
If anyone has any other ideas that I could try, I'd be grateful.
I finally managed to solve this. There was no change to the react component code but the InMemoryCacheOptions now looks like this:
const inMemoryCacheOptions = {
addTypename: true,
typePolicies: {
Repository: {
fields: {
releases: {
keyArgs: false,
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming;
const { nodes, ...rest } = incoming;
// We only need to merge the nodes array.
// The rest of the fields (pagination) should always be overwritten by incoming
let result = rest;
result.nodes = [...existing.nodes, ...nodes];
return result;
}
}
}
}
}
};
The main change from my original code is that I now define the typePolicy for the releases field of the Repository type. Previously I was trying to get directly to the nodes field of the Release type. Since my Repository type the root of the gql query and used in the component, it now reads the merged results from the cache.
If I specified the typePolicy for Query as mentioned in the docs, I would not be able to specify the merge behaviour for the releases field because it would be one level too deep (i.e. Query -> repository -> releases). This is what lead to my confusion in the beginning.

How to use detalization queries in apollo graphql reactjs?

Suppose data - is data from a parent query.
Child react-component:
const ShowDetails = ({data}) => {
const { loading, error, data_details } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache);
}
someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache -- asks for additional fields that are missing in data.
When (!loading && !error) data_details will have requested fields.
Issue: data_details will have only requested fields.
Question: Is there a way to use parent data with merged-additional-requested fields in ShowDetails and ignore data_details?
In Chrome with help of Apollo devtools I see that apollo-cache has one entry from merged data and data_details.
I do not want to re-fetch all existed entries in data.
Example:
Parent component query:
const bookQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
author
}
}
`
Details query:
const bookEditionsQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
editions {
publisher
year
}
}
}
`
const bookReviewQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
review {
user
score
date
}
}
}
`
All this queries will populate the same bucket in Apollo cache: book with id.
What is necessary to achieve: in react component BookDetails:
have 1 object with:
data.author
data.editions[0].year
data.review[0].user
Logically - this is one entry in cache.
Thank you for your help.
Almost nothing to save by using already fetched [and passed from parent] data ... only author ... all review and edition must be fetched, no cache usage at all.
... fetching review and editions by book resolver helps apollo cache to keep relation but also requires API to use additional ('book') resolver [level] while it is not required ... review and editions resolvers should be callable directly with book id ... and f.e. can be used by separate <Review /> sub component ... or review and editions called within one request using the same id parameter.
Just use data and dataDetails separately in component - avoid code complications, keep it simply readable:
const ShowDetails = ({data}) => {
const { loading, error, data:dataDetails } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache);
}
if(loading) return "loading...";
return (
<div>
<div>author: {data.author}</div>
{dataDetails.review.map(...
... if you really want to join data
const ShowDetails = ({data}) => {
const [bookData, setBookData] = useState(null);
const { loading, error, data:dataDetails } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache, {
onCompleted: (newData) => {
setBookData( {...data, ...newData } );
}
});
if(bookData) return ...
// {bookData.author}
// bookData.review.map(...

How can I have multiple sibling instances of the same top level fragment container?

I am following this guide: https://relay.dev/docs/en/quick-start-guide#composing-fragments
I am trying to create a higher level fragment container that queries data from the RootQuery:
export const FormInstanceListContainer: FunctionComponent<Props> = props => {
const { data, ...rest } = props
const { formInstances } = data
return (
<FormInstanceList
formInstances={formInstances}
{...rest} // Forward all the other props
/>
)
}
export default createFragmentContainer(FormInstanceListContainer, {
data: graphql`
fragment FormInstanceListContainer_data on RootQuery
#argumentDefinitions(status: { type: "FormStatus" }) {
formInstances(status: $status) {
id
form {
id
name
}
status
createdAt
submittedAt
}
}
`,
})
This works well as long as I only need one of these lists rendered. Here is a usage example:
const IndexPage: NextPage<QueryResponse> = data => {
const handleOpenClick = (formInstance: FormInstance) => {
NextRouter.push(`/form-instance/${formInstance.uuid}`)
}
const handleEditClick = (formInstance: FormInstance) => {
NextRouter.push(`/form-instance/${formInstance.uuid}/edit`)
}
return (
<DefaultLayout>
<Container maxWidth="md">
<Typography variant="h5" style={{ marginBottom: 25 }}>
My drafts
</Typography>
<FormInstanceListContainer
data={data}
onOpenClick={handleOpenClick}
onEditClick={handleEditClick}
/>
</Container>
</DefaultLayout>
)
}
export default withData<pages_dashboard_Query>(IndexPage, {
query: graphql`
query pages_dashboard_Query {
...FormInstanceListContainer_data #arguments(status: DRAFT)
}
`,
})
Unfortunately, I need 2 of these lists rendered side by side... One for draft forms and one for submitted forms.
I can't just include expand the same fragment again:
query pages_dashboard_Query {
...FormInstanceListContainer_data #arguments(status: DRAFT)
...FormInstanceListContainer_data #arguments(status: SUBMITTED)
}
ERROR:
Expected all fields on the same parent with the name or alias 'formInstances' to have the same name and arguments.
How then can I have more than one FormInstanceListContainer on the same page with different data? Have I hit a dead end the way I've designed my fragment container?
The problem would be solved if I was able to run the queries in the top level page (since then I could use aliases) and pass the list of results to the FormInstanceListContainer. To do that it seems to me that the FormInstanceListContainer must request the fields of the query rather than the query itself:
export default createFragmentContainer(FormInstanceListContainer, {
formInstance: graphql`
fragment FormInstanceListContainer_formInstance on FormInstance {
id
form {
id
name
}
status
createdAt
submittedAt
}
`,
})
However, now Relay assumes that a single instance of FormInstance should be passed to the container, not a list of them. Any way I try to pass a list causes Relay to crash. Is it possible to instruct Relay that it should expect a list of values rather than a single value?
I am completely stuck.
I think I've encountered this issue, and I think I solved it as you suggest, by including the fields that needs to aliased up in the query:
graphql`
fragment FormInstanceFields on FormInstance {
id
form {
id
name
}
status
createdAt
submittedAt
}
`
export default withData<pages_dashboard_Query>(IndexPage, {
query: graphql`
query pages_dashboard_Query {
drafts: formInstances(status: DRAFT) {
...FormInstanceFields
}
submitted: formInstances(status: SUBMITTED) {
...FormInstanceFields
}
}
`,
})
(Above fragment needs to be named correctly per compiler requirements)
Then the thing to do is to not wrap FormInstanceList in a fragment container, because as you point out, this leads to errors. The impulse to wrap it is strong, because using the HOC-based API, the instinct is to wrap all the way down through the render tree. But that won't work in this case.
Instead, wrap each FormInstance in a fragment container (which I'm guessing you're doing already already). Then it will work. The drafts and submitted fields will contain arrays of Relay store refs, and you can interate over them and pass each one to FormInstanceContainer.
This may feel wrong, but if you think of it from the perspective of the upcoming hooks API, or relay-hooks, there's nothing really wrong it.
EDIT
Haven't used Relay in a while, but I think you could even keep the containers at all levels by doing something like this:
graphql`
fragment FormInstanceListContainer_data on RootQuery {
drafts: formInstances(status: DRAFT) {
...FormInstanceFields
}
submitted: formInstances(status: SUBMITTED) {
...FormInstanceFields
}
}
`
Shouldn't that work, too?

React: setState after a graphQL request

I've been searching for a couple of hours now, but just can't seem to find the answer. See my code below. I'm requesting some metro-information to be used on an info-screen.
I'm getting the information, seeing as console.log works. However I'm having difficulty using this resulting oject. I want to use the data received, so that I can display when the next train arives. To this purpose I try to setState with the result, so that I can access the data-elements further down. However, now I'm stuck at setState giving me problems. I feel that I need to bind the function, but this.main = this.main.bind(this) doesn't work.
import React from "react";
import { GraphQLClient } from "graphql-request";
class Rutetider extends React.Component {
constructor(props) {
super(props);
this.state = {
stoppestedet: "rutetider lastes ned"
};
async function main() {
const endpoint = "https://api.entur.org/journeyplanner/2.0/index/graphql";
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
ET: "lossfelt-tavle"
}
});
const query = `
{
stopPlace(id: "NSR:StopPlace:58249") {
id
name
estimatedCalls(timeRange: 3600, numberOfDepartures: 20) {
realtime
aimedArrivalTime
aimedDepartureTime
expectedArrivalTime
expectedDepartureTime
actualArrivalTime
actualDepartureTime
cancellation
notices {
text
}
situations {
summary {
value
}
}
date
forBoarding
forAlighting
destinationDisplay {
frontText
}
quay {
id
}
serviceJourney {
journeyPattern {
line {
id
name
transportMode
}
}
}
}
}
}
`;
const data = await graphQLClient.request(query);
console.log(data);
this.setState({ stoppestedet: data.stopPlace.name });
}
main().catch(error => console.error(error));
}
render() {
return (
<div>
rutetider
<div className="grid-container2">
<div>Mot byen</div>
<div>fra byen</div>
<div>{this.state.stoppestedet}</div>
</div>
</div>
);
}
}
export default Rutetider;
"probably easier" is to use integrated solution (apollo) than minimal, low level library. In most cases (as project grows), with more components fetching data managing separate GraphQLClient for all of them won't be an optimal solution. Apollo gives you centralised "fetching point", cache .. and many more.
Syntax error comes from function - in class it's enough to write async main()
https://codesandbox.io/s/l25r2kol7q
It probably would be better to save entire data in state and extract needed parts later (at render) and use this object as 'data-ready flag' (as I did for place - 'stoppestedet') - initally undefined (in constructor) for initial render (conditional rendering, some <Loading /> component):
render() {
if (!this.state.stoppestedet) return "rutetider lastes ned";
return (
<div>
rutetider
<div className="grid-container2">
<div>Mot byen</div>
<div>fra byen</div>
<div>{this.renderFetchedDataTable()}</div>
</div>

reactjs and redux: How to implement filter text box for a rows of data

I am learning reactj and redux. Somehow i managed to get data from api and display them in table. The table is shown below.
I want to add filter text box below each column header so that when i type text, it shows those values matching the results.
What is the flow using redux. Or any example
I just typed this up, its just for a general idea of how to filter data in your mapstate function. This is called computing derived data https://redux.js.org/docs/recipes/ComputingDerivedData.html and its good to use tools like Reselect for performance benefits. This isnt working code, but its just to give you an idea of how it is usually done so you dont get duplicate state in your reducers.
class Table extends React.Component {
filterData = (event, column) => {
this.props.someFilterAction(event.target.value, column)
}
renderItem = (item) => {
return <someItemDiv searchAction={(e) => this.filterData(e, item.column) } />
}
render(){
<div>
{ this.props.data.map(this.renderItem) }
</div>
}
}
function mapStateToProps(state) {
const searchItem = state.searchData;
// reselect is good for stuff like this
const filteredData = state.tableData.filter(item => item[searchItem.column].text.indexOf(searchItem.input) !== -1 )
// or some kind of search criteria you want
return {
data: filteredData,
searchItem: state.searchData
}
}
const mapDispatchToProps = {
someFilterAction: YourReduxActionCreator
}
export default connect(mapStateToProps, mapDispatchToProps)(Table);
function YourReduxActionCreator(input, column) {
return {
type: FILTER_DATA,
payload: {
column,
input,
}
}
}
function yourReducer() {
switch(state, action) {
case FILTER_DATA:
return action.payload
default:
return state;
}
}
Hey you just need to create 2 selectors.
The first one : getTableData() that get your whole table data from your store
Then getFilteredTableData() which will filter the result of getTableData() using the form values of your field (your keyword and the column).
The selector getFilteredTableData() should be the source of data for your table.
So as soon as your form change your table will be refiltered !

Resources