I have a React Component that calls a Query that expects to receive an array of objects (for validation data). I then use it to validate an html form, that is in turn defined inside of an apollo Mutation element. The way it is structured within the return statement of the component render() method works but it looks cumbersome and it reminds me of the callback hell era. What I really want to do is to get rid of the <QueryCustomerValidations> element in the render() method and move it into (ideally) the componentDidMount lifecycle event. So that the validators can be loaded there for later use within the form, then leave the <MutationCreateCustomer>inside render().
Right now, I have to wrap the form inside the mutation. The mutation inside the arrow function that the query returns in order to receive the async data in the proper order.
//-------------------------------------------------------------------------
// Component Lifecycle Eventhandler Methods
//-------------------------------------------------------------------------
componentDidMount()
{
}
//-------------------------------------------------------------------------
// Render Method Section
//-------------------------------------------------------------------------
public render(): JSX.Element
{
// Return Form
return (
<React.Fragment>
{/* PAGE TITLE */}
<h2 className="text-center mb-3">Agregar Nuevo Cliente</h2>
{/* LOAD VALIDATIONS INTO STATE */}
<QueryCustomerValidations
query={Q_GET_CUSTOMER_VALIDATIONS}
>
{({ loading: loadingValidations, error: errorValidations, data: dataValidations }) =>
{
if (loadingValidations)
{
return "Cargando..."
}
if (errorValidations)
{
return `Error: ${errorValidations.message}`
}
if (dataValidations)
{
const validators: ValidationDescriptor[] = []
dataValidations.getCustomerValidations.forEach((validator) => {
validators.push(validator as ValidationDescriptor)
})
this.validators.setValidators(validators)
}
/* DATA ENTRY FORM */
return (
<div className="row justify-content-center">
<MutationCreateCustomer
mutation={M_CREATE_CUSTOMER}
onCompleted={() => this.props.history.push('/')}
>
{(createCustomer: any) => {
return (
<form name="frmNewCustomer"
className="col-md-8 m-3"
onSubmit={e => this.frmNewCustomer_submit(e, createCustomer)}
>
{ this.ctrl_form_layout(this.props, this.state, this.validators) }
</form>
)
}}
</MutationCreateCustomer>
</div>
)
}}
</QueryCustomerValidations>
</React.Fragment>
);
}
Here for documentation purposes are the interfaces to create the Query. Since I get some of this data from the server using the apollo client, a simple graphql query solution on the onDidMount() would not work in this case.
getCustomerValidations.ts (Interfaces)
// ====================================================
// GraphQL query operation: getCustomerValidations
// ====================================================
export interface getCustomerValidations_getCustomerValidations {
__typename: "ValidationDescriptor";
field: string | null;
type: string | null;
required: boolean;
max: number | null;
min: number | null;
regex: string | null;
}
export interface getCustomerValidations {
getCustomerValidations: getCustomerValidations_getCustomerValidations[];
}
customer-validations.query.ts (Client side query types)
//---------------------------------------------------------------------------------
// Imports Section (React/Apollo Libs)
//---------------------------------------------------------------------------------
import { gql } from 'apollo-boost';
import { Query } from 'react-apollo'
import { getCustomerValidations } from '../../typeDefs/operations/getCustomerValidations'
//---------------------------------------------------------------------------------
// GQL Query: Customers
//---------------------------------------------------------------------------------
export const Q_GET_CUSTOMER_VALIDATIONS = gql`
query getCustomerValidations {
getCustomerValidations
{
field
type
required
max
min
regex
}
}
`;
//---------------------------------------------------------------------------------
// Query Class: Customers
//---------------------------------------------------------------------------------
export class QueryCustomerValidations extends Query<getCustomerValidations> { }
The right solution can be a way to copy -and trigger- the <QueryCustomerValidations>element from the componentDidMount() method or, how to take the element away from the render() method and call it like with some sort of "await" way of doing it so it can be called first and the mutation after (and using the data from the query).
Thanks, I know this one is not really easy to figure out.
You're looking for 'old' (used before <Query/> component) HOC pattern (with compose) described here, here and here.
Composing "graphql(gqlquery ..." requested at start (without conditional skip option) with one or more (named) "gqlmutation ..." (called on demand) gives you a clear, readable solution.
compose(
graphql(Q_GET_CUSTOMER_VALIDATIONS),
graphql(gql`mutation { ... }`, { name: 'createSth' })
)(SomeComponent)
will pass data and createSth props to <SomeComponent/> then this.props.data object will contain loading, error and getCustomerValidations fields/properties. It's described here.
Query will be called at start (you can expect true in this.props.data.loading), no need to run query at cDM(). Mutation can be run using this.props.createSth() - custom name defined above (instead default prop name mutate).
Of course you can mix them with other required HOC's, f.e. redux connect(), withFormik() etc. - simply by adding single line of code.
Related
I have read the old related post from StackOveflow, but seems that the API has changed. I am using React with GraphQL and Strapi. My graphql query has ID parameters in it. Despite I checked the documentation and I am not able to see where this warning comes from.
The ID of all the backend data collections is managed by Strapi.
If you could help, I would owe you a lot!
The warning I received is this:
Cache data may be lost when replacing the attributes field of a CategoryEntity object.
To address this problem (which is not a bug in Apollo Client), either ensure all objects of type Category have an ID or a custom merge function, or define a custom merge function for the CategoryEntity.attributes field, so InMemoryCache can safely merge these objects:
existing: {"__typename":"Category","name":"Worms","reviews({\"sort\":\"createdAt:desc\"})":{"__typename":"ReviewRelationResponseCollection","data":[{"__ref":"ReviewEntity:5"}]}}
incoming: {"__typename":"Category","name":"Worms"}
For more information about these options, please refer to the documentation:
* Ensuring entity objects have IDs: https://go.apollo.dev/c/generating-unique-identifiers
* Defining custom merge functions: https://go.apollo.dev/c/merging-non-normalized-objects
Please find bellow my full code:
import React from 'react'
import { useQuery, gql } from '#apollo/client'
import { useParams, Link } from 'react-router-dom'
const CATEGORY= gql`
query GetCategory($id: ID!) {
category(id: $id) {
data
{
id
attributes
{
name
reviews (sort: "createdAt:desc") {
data
{
id
attributes
{
title
body
rating
createdAt
categories{
data
{
id
attributes{
name
}
}
}
}
}
}
}
}
}
}
`
export default function Category() {
const { id } = useParams()
const { loading, error, data } = useQuery(CATEGORY, {
variables: { id: id }
})
if (loading) return <p>Loading...</p>
if (error) return <p>`Error! ${error}`</p>
console.log(data)
return (
<div>
<h2>{ data.category.data.attributes.name } Games</h2>
{data.category.data.attributes.reviews.data.map(review => (
<div key={review.id} className="review-card">
<div className="rating">{review.attributes.rating}</div>
<h2>{review.attributes.title}</h2>
<h5>{review.attributes.createdAt.substring(0, 10)}</h5>
{review.attributes.categories.data.map(c => (
<small key={c.id}>{c.attributes.name}</small>
))}
<p>{review.attributes.body.substring(0, 200)}...</p>
<Link to={`/details/${review.id}`}>Read more</Link>
</div>
))}
</div>
)
}
Based on this thread inside the Apollo Community.
It is more just a friendly note to make extra sure you’re implementing pagination correctly. In this case since it’s already doing exactly what you want, it is totally safe to ignore the warning. Warnings get silenced when you build for production so it’s just noisy while developing. In my opinion it is a little too noisy, but it doesn’t hurt anything.
I need to call an api which consists of an array of string. I need to then publish the response from the api in a dropdown menu. Below is what the API holds that I need to call-
Sample api data - [“Leanne Graham”,”Ervin Howell”,”Patricia”]
Below sample code has the API which holds object information
import React, { Component } from "react";
import "../styles/schema.css";
import Params1 from "../components/Params1";
import axios from 'axios';
import Select from "react-select";
class New extends Component {
constructor(props) {
super(props);
this.handleStoreprocSelection = this.handleStoreprocSelection.bind(this);
this.state = {
selectStoreprocOptions : [],
id: "",
name: '',
itemSelected:false
}
}
async getStoreProcOptions(){
const resSchema = await axios.get('https://jsonplaceholder.typicode.com/users') --backend API call in object format
const data = resSchema.data
const options = data.map(d => ({
"value" : d.id,
"label" : d.name
}))
this.setState({selectStoreprocOptions: options})
}
handleStoreprocSelection(){
// alert('You selected me!!')
this.setState({itemSelected: true});
}
componentDidMount() {
// get all entities - GET
this.getStoreProcOptions()
}
render() {
const itemSelected = this.state.itemSelected;
let param;
if (itemSelected) {
param = <Params1 />;
}
return (
<div>
<div>
<form id ="store-proc">
<label>STORED PROCEDURE</label>
<Select id="store-proc-select" options={this.state.selectStoreprocOptions} onChange={this.handleStoreprocSelection} /> --my dropdown
</form>
</div>
{param}
</div>
);
}
}
export default New;
You need a state, let's say an empty array.
You need to call that API, using some of the methods, for example browser built in fetch or 3rd party library axios.
Then you need to update your state with the response you will get from your API.
Finally use your state inside of your component and display whatever you want.
These are the steps you need to follow, if you needed some logic. Since you didn't provide any code, I assume you didn't know from where to start. If you share some code, will be possible to help more.
are you using any library? because the plain HTML form select would be written in lower case <select/>, not <Select/>. if so, please state it out.
in plain HTML: the solution would be to map the array elements into <option/>. so, selectStoreprocOptions from state, as assigned here: this.setState({selectStoreprocOptions: options}).
inside render:
<select>
{
this.state.selectStoreprocOptions.map(selectStoreprocOption => (<option ..> .. </option>)
}
</select>
Edit: Sorry, I've overseen the use of react-select. never used it, according to the API doc it looks good to me. have you checked that your state really contains an array with the expected objects?
probably ignore my post then, sorry again xD
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?
My application looks something like what's included in the snippets below.
(I've left out a lot of the implementation details in the hopes that the code below is sufficient to get my question across).
SampleApp.js
const SampleAppQuery = graphql`
list SampleAppQuery {
...ItemComponent_item
}
`
function SampleApp() {
return (
<QueryRenderer
environment={environment}
query={SampleAppQuery}
render={(error, props) => {
if (props) {
return <AppRefetchContainer list={props} />
}
}}
/>
)
}
AppRefetchContainer.js
class AppRefetchContainer extends Component {
render() {
<div>
<HeaderComponent refetchList={this._refetchList} />
{list.map(item => <ItemComponent item={item} />)}
</div>
}
_refetchList(params) {
this.props.relay.refetch(params)
}
}
export default createRefetchContainer(
AppRefetchContainer,
graphql`
fragment AppRefetchContainer_list on Item {
...ItemComponent_item
}
`,
graphql`
query AppRefetchContainerQuery($sortBy: String!)
list SampleAppQuery(sortBy: $sortBy) {
...ItemComponent_item
}
`
)
The initial application load is just fine. Clicking on one of the headers should trigger a refetch so that the list data can be sorted on the passed inparams. When I trigger a refetch, the server responds with the correct data, however, the component does not rerender.
I suspect that my issue is with the way I've defined the fragments here. The console error I get on initial page load is:
RelayModernSelector: Expected object to contain data for fragment
`AppRefetchContainer_list`, got `{"list":[{"__fragments":
{"ItemComponent_item":{}},"__id":"[some_valid_id]","__fragmentOwner":null},{...
Question(s):
I'm not entirely sure how to interpret that error. Is it the reason why I'm unable to rerender the UI?
I know this approach may not be ideal, but how do I refetch from a component that isn't necessarily a fragment of the query?
Happy to provide any clarification or missing context :)
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>