I'm trying tok create a simple demo CRUD app using React and Graphql
I have the backend all set and working.
On the front end I'm using React with useQuery and useMutation.
I have the create working with useMutation to create the data and the read with useQuery to show the data.
I now stuck with the delete. I have a button the post and I can get the id of the post to pass to the useMutation function to delete the post.
I'm just stuck getting it to work if anyon can help.
App.tsx
import React, { useState } from 'react';
import './App.css';
import { RecipeData } from '../generated/RecipeData';
import { GET_ALL_RECIPES, ADD_RECIPE, DELETE_RECIPE } from '../queries';
import { useQuery, useMutation } from 'react-apollo-hooks';
const App: React.FC = () => {
const [name, setName] = useState<string>('')
const [description, setDes] = useState<string>('')
const [id, setId] = useState<string>('')
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}
const handleDesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDes(e.target.value)
}
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
createRecipe()
};
const handelDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
setId(e.target.parentElement.getAttribute("data-id"))
deleteRecipe()
}
const [deleteRecipe] = useMutation(DELETE_RECIPE, {
variables: { id }, refetchQueries: ['RecipeData']
})
const [createRecipe, { error }] = useMutation(ADD_RECIPE, {
variables: { name, description }, refetchQueries: ['RecipeData']
})
if (error) {
console.error('erroring : ', error)
}
const { data, loading } = useQuery<RecipeData | null>(GET_ALL_RECIPES, {
suspend: false
})
if (loading || !data) return <div>Loading</div>
return (
<div className="App">
<h1>Graphql</h1>
<ul>
{
data.recipe !== null && data.recipe.map((recipe, i) => (
<li key={recipe._id} data-id={recipe._id}>
{recipe.name}
<button onClick={handelDelete}>X</button>
</li>
))
}
</ul>
<form>
<div>
<label>Name</label>
<input
type="text"
value={name}
onChange={handleNameChange}
/>
</div>
<div>
<label>Description</label>
<input
type="text"
value={description}
onChange={handleDesChange}
/>
</div>
<button onClick={handleClick}>Add Recipe</button>
</form>
</div>
);
}
export default App;
queries/index.tsx
import { gql } from 'apollo-boost';
export const GET_ALL_RECIPES = gql`
query RecipeData{
recipe{
_id
name
description
}
}
`
export const ADD_RECIPE = gql`
mutation addRecipe($name: String!, $description:String){
addRecipe(name: $name, description: $description){
_id
name
description
}
}
`
export const DELETE_RECIPE = gql`
mutation deleteRecipe($id: Int!){
deleteRecipe(_id: $id){
_id
name
description
}
}
`
Server side
schema.js
exports.typeDefs = `
type Recipe{
_id: ID
name: String
description: String
}
type Query{
recipe:[Recipe]
}
type Mutation{
addRecipe(name: String!, description: String):Recipe
deleteRecipe(_id: Int):Recipe
}
`
resolvers.js
exports.resolvers = {
Query:{
recipe: async(obj, args, {Recipe}, info) => {
const allRecipes = await Recipe.find()
return allRecipes
}
},
Mutation:{
addRecipe: async(obj, {name, description}, {Recipe}, info) => {
const newRecipe = await new Recipe({
name,
description
}).save()
return newRecipe
},
deleteRecipe: async(obj, args, {Recipe}, info, {_id}) => {
delete Recipe[_id];
}
}
}
I don't have a live demo sorry but this outputs a list of recipe name with an input field to add new recipes, this part works.
I also have a delete button after each recipe displayed. I wanted to click this button and have that recipe be removed from the list.
At the moment when I click this delete button I get the following error message in the network tab of the dev tools
message: "Variable "$id" got invalid value ""; Expected type Int. Int cannot represent non-integer value: """
const handelDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
setId(e.target.parentElement.getAttribute("data-id"))
deleteRecipe()
}
setID() is needed for something (other than passing parameter) ?
id is not updated immediately by setID() (async updates like setState - see docs) - value can't be used in following deleteRecipe()
IMHO there should be:
const handelDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
const delID = e.target.parentElement.getAttribute("data-id")
deleteRecipe( delID )
}
and deleting with direct parameter passing:
const deleteRecipe = (id) => useMutation(DELETE_RECIPE, {
variables: { id }, refetchQueries: ['RecipeData']
})
Related
I am trying to figure out how to access the object.id on the update mutation I have made in my app.
I have an object called IssueGroup. I have made create and delete mutations and for the most part, they work okay. I'm getting stuck with the update mutation because I can't seem to get it to recognise the id of the object I'm trying to update.
I have a form with:
import * as React from "react"
import { gql } from "#apollo/client"
import type { IssueGroupInput } from "lib/graphql"
import { QueryMode, Role, SortOrder, useAllIssueGroupsQuery, useDeleteIssueGroupMutation, useUpdateIssueGroupMutation } from "lib/graphql"
import { AdminCreateIssueGroupForm } from "components/AdminCreateIssueGroupForm"
import { AdminUpdateIssueGroupForm } from "components/AdminUpdateIssueGroupForm"
import { Modal } from "components/Modal"
const __ = gql`
query AllIssueGroups {
allIssueGroups {
id
title
description
}
}
mutation deleteIssueGroup($id: String!) {
deleteIssueGroup(id: $id) {
id
title
description
issues
}
}
`
export default function IssueGroups() {
const [selectedIssueGroups, setSelectedIssueGroups] = React.useState<string[]>([])
const modalProps = useDisclosure()
const modalPropsUpdate = useDisclosure()
const [deleteIssueGroup] = useDeleteIssueGroupMutation()
const [updateIssueGroup] = useUpdateIssueGroupMutation()
const { data: issueGroup, refetch: refetchAllIssueGroups } = useAllIssueGroupsQuery()
const { data, loading, refetch } = useAllIssueGroupsQuery()
const allIssueGroups = data?.allIssueGroups
const onDeleteIssueGroup = (id: string) => {
return (
deleteIssueGroup({ variables: { id } }).then(() => refetch())
)
}
return (
<Box>
<Wrap mb={4} spacing={2}>
<Button
onClick={modalProps.onOpen}
}}
>
Create issue group
</Button>
</Wrap>
<Modal {...modalProps} title="Create Issue Group">
<AdminCreateIssueGroupForm onClose={modalProps.onClose} />
</Modal>
<IconButton
onClick={modalPropsUpdate.onOpen}
/>
<Modal {...modalPropsUpdate} title="Update Issue Group">
<AdminUpdateIssueGroupForm onClose=
{modalPropsUpdate.onClose} />
</Modal>
<IconButton
onClick={() => onDeleteIssueGroup(issueGroup.id)}
/>
))}
)
}
Then, I have a modal that opens when the update button is clicked that has:
import * as React from "react"
import { gql } from "#apollo/client"
import { useRouter } from "next/router"
import { useAllIssueGroupsQuery, useUpdateIssueGroupMutation, IssueGroupInput } from "lib/graphql"
import { useForm } from "lib/hooks/useForm"
import Yup from "lib/yup"
const _ = gql`
query AllIssueGroups {
allIssueGroups {
id
title
description
issues
}
}
mutation updateIssueGroup($id: String!, $data: IssueGroupInput!) {
updateIssueGroup(id: $id, data: $data) {
id
title
description
issues
}
}
`
interface Props {
onClose: () => void
}
const IssueGroupSchema = Yup.object().shape({
title: Yup.string().required(),
description: Yup.string().required(),
})
export function AdminUpdateIssueGroupForm(props: Props) {
const router = useRouter()
const [updateIssueGroup] = useUpdateIssueGroupMutation()
const { data: issueGroups, refetch: refetchAllIssueGroups } = useAllIssueGroupsQuery()
const form = useForm({ schema: IssueGroupSchema })
const handleSubmit = async(data:IssueGroupInput) => {
console.log("made it to the handle submit before success handler")
return await form.handler(() => updateIssueGroup({
variables: {
id: issueGroup.id,
// I get an error that says Cannot find name 'issueGroup'. Did you mean 'issueGroups'.
// I know that's because I'm using the AllIssueGroups query to find many,
// but I don't know how to write the query to find the specific one I
// want to edit when I press the edit button in the form above
data: { ...data }
} }), {
onSuccess: (res, toast) => {
console.log("made it to the form handler success")
toast({ description: "Issue group updated" })
form.reset()
props.onClose()
},
})
}
return (
<Form {...form} onSubmit={handleSubmit}>
<Stack>
<Input name="title" label="Title" />
{/* <Input name="description" label="Description" /> */}
{/* <Text mb='8px' fontWeight="medium" fontSize="sm" > Description</Text> */}
<Textarea name="description" label="Describe" rows={4}/>
<Button onClick={props.onClose}>Cancel</Button>
type="submit"
>
Create
</Form>
)
}
My IssueGroup resolver has:
#Mutation(() => IssueGroup)
async updateIssueGroup(
#Arg("id") id: string,
#Arg("data") data: IssueGroupInput
) {
return await this.issueGroupService.updateIssueGroup(id, data)
}
#Query(() => IssueGroup)
async issueGroup(#Arg("id") id: string) {
return await this.issueGroupService.getIssueGroup(id)
}
The IssueGroup service has:
async updateIssueGroup(id: string, data: IssueGroupInput) {
const issueGroup = await prisma.issueGroup.findUnique({ where: { id } })
if (!issueGroup) {
throw new Error("Issue not found")
}
return await prisma.issueGroup.update({ where: { id }, data })
}
async getIssueGroup(id: string) {
return await prisma.issueGroup.findUnique({
where: {
id,
},
})
}
How can I tell the modal form for update what the specific issueGroup.id related to the button clicked to open the modal is?
The prisma schema has:
model IssueGroup {
id String #id #default(dbgenerated("gen_random_uuid()")) #db.Uuid
title String
description String
issues Issue[]
createdAt DateTime #default(now()) #db.Timestamptz(6)
updatedAt DateTime #default(now()) #updatedAt #db.Timestamptz(6)
}
model Issue {
id String #id #default(dbgenerated("gen_random_uuid()")) #db.Uuid
title String
description String
issueGroup IssueGroup? #relation(fields: [issueGroupId], references: [id], onDelete: SetNull, onUpdate: Cascade)
issueGroupId String? #db.Uuid
subscribers UserIssue[]
createdAt DateTime #default(now()) #db.Timestamptz(6)
updatedAt DateTime #default(now()) #updatedAt #db.Timestamptz(6)
}
NEXT ATTEMPT
I have made a further attempt, which tries to give the modal the issueGroup.id with the onClick handler, but I still can't get the form to recognise the id, and this version of this attempt generates an error message I don't know how to break down. It says:
Type '{ onClose: () => void; issueGroup: Omit<IssueGroup, "createdAt"
| "updatedAt" | "issues"> | null; }' is not assignable to type
'IntrinsicAttributes & Props'. Property 'issueGroup' does not exist
on type 'IntrinsicAttributes & Props'.
This time:
I tried to set state:
import { QueryMode, Role, SortOrder, useAllIssueGroupsQuery, useDeleteIssueGroupMutation, useUpdateIssueGroupMutation, IssueGroup as IssueGroupGQLType } from "lib/graphql"
const [selectedIssueGroup, setSelectedIssueGroup] = React.useState<Omit<
IssueGroupGQLType,
"createdAt" | "updatedAt" | "issues"
> | null>(null)
the update button has:
<IconButton
aria-label='Update Issue Group'
// onClick={modalPropsUpdate.onOpen}
onClick={() => {
setSelectedIssueGroup(issueGroup)
modalPropsUpdate.onOpen()
<Modal {...modalPropsUpdate } title="Update Issue Group">
<AdminUpdateIssueGroupForm onClose={modalPropsUpdate.onClose} issueGroup ={selectedIssueGroup} />
</Modal>
Then, in the form, I tried to read issueGroup.id:
const _ = gql`
query AllIssueGroups {
allIssueGroups {
id
title
description
issues
}
}
mutation updateIssueGroup($id: String!, $data: IssueGroupInput!) {
updateIssueGroup(id: $id, data: $data) {
id
title
description
issues
}
}
`
interface Props {
onClose: () => void
issueGroup: selectedIssueGroup
}
const IssueGroupSchema = Yup.object().shape({
title: Yup.string().required(),
description: Yup.string().required(),
})
export function AdminUpdateIssueGroupForm(props: Props) {
const router = useRouter()
const [updateIssueGroup] = useUpdateIssueGroupMutation()
// const { data: issueGroups, refetch: refetchAllIssueGroups } = useAllIssueGroupsQuery()
const form = useForm({ schema: IssueGroupSchema })
const handleSubmit = async( data:IssueGroupInput) => {
// await form.triggerValidation()
console.log("made it to the handle submit before success handler")
The variables below don't know what issueGroup is
return await form.handler(() => updateIssueGroup({ variables: { id: issueGroup.id, data: { ...data } } }), {
onSuccess: (res, toast) => {
console.log("made it to the form handler success")
form.reset()
props.onClose()
},
})
}
return (
<Form {...form} onSubmit={handleSubmit}>
<Stack>
<Input name="title" label="Title" />
<Textarea name="description" label="Describe" rows={4}/>
<FormError />
<ButtonGroup>
<Button onClick={props.onClose}>Cancel</Button>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
isDisabled={form.formState.isSubmitting}
>
Create
</Button>
</ButtonGroup>
</Stack>
</Form>
)
}
With this attempt, my form still doesnt know what selectedIssueGroup or IssueGroup mean.
I have seen this page of the react docs, and I think the bit I am missing is that I haven't wrapped my form in the equivalent of isActive. The problem is, I don't know where to put selectedIssueGroup. My definition of that state is in a different file to the file the form is is saved in.
In an attempt to apply the logic in this documentation, I tried changing the modal so that is wrapped inside the state, as follows:
{selectedIssueGroup &&
<Modal {...modalPropsUpdate } title="Update Issue Group">
<AdminUpdateIssueGroupForm onClose={modalPropsUpdate.onClose} issueGroup ={selectedIssueGroup} />
</Modal>
}
I don't get any new errors, but it also doesn't work. My form still doesn't know what the issueGroup is.
I can see that the update button knows what the issueGroup is and I can see that the Modal knows it too. I cant figure out how to give that value to the update form.
I have seen this tutorial, which shows how to use the create form to make an edit. The screen shot it uses displays the created information populated in the edit form, but it does not show how it found that data to use in that form. I can't find an example of how to do that.
NEXT ATTEMPT 21 DEC
In this most recent attempt, I have tried to add the issueGroup reference as a property on the Modal. Even if this worked, I think it would not be helpful for a reason explained below. However, i don't understand why it doesn't work.
<Modal {...modalPropsUpdate } issueGroup={selectedIssueGroup} title="Update Issue Group" >
When I try this, I get an error (VS code underlines the issueGroup in the above line). The error message says:
Type '{ children: (void | Element)[]; issueGroup: Omit<IssueGroup, "createdAt" | "updatedAt" | "issues"> | null; title: string; isOpen:
boolean; onOpen: () => void; ... 4 more ...; getDisclosureProps:
(props?: any) => any; }' is not assignable to type
'IntrinsicAttributes & Props & { children?: ReactNode; }'.
Property 'issueGroup' does not exist on type 'IntrinsicAttributes & Props & { children?: ReactNode; }'.
I don't know what this means. The console logs inside the update button, and above and below the modal all know what issueGroup is.
The reason I think it is not required is that I can log the value of the issue Group above the modal and above the component inside the modal that loads the form. Both logs show the value of the issueGroup that I want to use in the form.
I'm trying to do that as follows:
const CheckIfModalKnowsIssueGroupId = props => {
console.log("checking if modal knows the issue group", props.toLog);
return (
<div>
{props.children}
</div>
);
};
ALTERNATE for attempt to log inside the form component
<AdminUpdateIssueGroupForm
onClose={modalPropsUpdate.onClose}
issueGroup ={selectedIssueGroup}
>
<CheckIfModalKnowsIssueGroupId toLog={selectedIssueGroup} />
</AdminUpdateIssueGroupForm>
NEXT ATTEMPT - READ MODAL VALUES
In a further attempt to try and make the modal carry the value of IssueGroup, I am trying to do the following:
{selectedIssueGroup && modalPropsUpdate(isOpen:true) <Modal {...modalPropsUpdate } issueGroup={selectedIssueGroup} title="Update Issue Group" >
This attempt is wrong because I don't know how to engage with modalPropsUpdate. The above formulation generates an error in the isOpen test as follows:
const modalPropsUpdate: {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
onToggle: () => void;
isControlled: boolean;
getButtonProps: (props?: any) => any;
getDisclosureProps: (props?: any) => any;
}
This expression is not callable. Type '{ isOpen: boolean; onOpen: ()
=> void; onClose: () => void; onToggle: () => void; isControlled: boolean; getButtonProps: (props?: any) => any; getDisclosureProps:
(props?: any) => any; }' has no call signatures.
I can't find syntax that does not produce an error.
The objective of this attempt is to see if the modal is open, and still knows what the selectedIssueGroup is.
NEW OBSERVATION
I have seen this repo. I cannot find an example using the structure I have adopted to allow the id to be communicated to the form.
NEXT ATTEMPT 23 DEC
I tried to rule out the source of the problem as the selectedIssueGroup state handler being set in a different component to the form I was trying to use to update the object. So, I moved the form into the same file. It did not work. The udpate form handler still does not know what issueGroup.id or selectedIssueGroup mean.
import * as React from "react"
import { gql } from "#apollo/client"
import { useRouter } from "next/router"
import type { IssueGroupInput } from "lib/graphql"
import { QueryMode, Role, SortOrder, useAllIssueGroupsQuery, useDeleteIssueGroupMutation, useUpdateIssueGroupMutation, IssueGroup as IssueGroupGQLType } from "lib/graphql"
// ,
import { AdminCreateIssueGroupForm } from "components/AdminCreateIssueGroupForm"
import Yup from "lib/yup"
const __ = gql`
query AllIssueGroups {
allIssueGroups {
id
title
description
}
}
mutation updateIssueGroup($id: String!, $data: IssueGroupInput!) {
updateIssueGroup(id: $id, data: $data) {
id
title
description
issues
}
}
`
const CheckIfModalKnowsIssueGroupId = props => {
console.log("checking if modal knows the issue group", props.toLog);
return (
<div>
{props.children}
</div>
);
};
const IssueGroupSchema = Yup.object().shape({
title: Yup.string().required(),
description: Yup.string().required(),
})
const form = useForm({ schema: IssueGroupSchema })
export default function IssueGroups() {
const [selectedIssueGroups, setSelectedIssueGroups] = React.useState<string[]>([])
const modalProps = useDisclosure()
const modalPropsUpdate = useDisclosure()
const [deleteIssueGroup] = useDeleteIssueGroupMutation()
const { data: issueGroup, refetch: refetchAllIssueGroups } = useAllIssueGroupsQuery()
const [selectedIssueGroup, setSelectedIssueGroup] = React.useState<Omit<
IssueGroupGQLType,
"createdAt" | "updatedAt" | "issues"
> | null>(null)
interface Props {
onClose: () => void
issueGroup: typeof selectedIssueGroup
}
function AdminUpdateIssueGroupForm(props: Props) {
const router = useRouter()
const [updateIssueGroup] = useUpdateIssueGroupMutation()
const handleSubmitUpdate = async( data:IssueGroupInput) => {
// await form.triggerValidation()
console.log("made it to the handle submit before success handler")
return await form.handler(() => updateIssueGroup({ variables: { id: issueGroup.id, data: { ...data } } }), {
onSuccess: (res, toast) => {
console.log("made it to the form handler success")
toast({ description: "Issue group updated" })
form.reset()
props.onClose()
},
})
}
const { data, loading, refetch } = useAllIssueGroupsQuery(
)
const allIssueGroups = data?.allIssueGroups
const onDeleteIssueGroup = (id: string) => {
return (
deleteIssueGroup({ variables: { id } }).then(() => refetch())
)
}
return (
<Wrap mb={4} spacing={2}>
<Button
onClick={modalProps.onOpen}
>
Create issue group
</Button>
</Wrap>
<Modal {...modalProps} title="Create Issue Group">
<AdminCreateIssueGroupForm onClose={modalProps.onClose} />
</Modal>
{data?.allIssueGroups.map((issueGroup) => (
<Tr key={issueGroup.id}>
<Text textStyle="h6">{issueGroup.title}</Text>
<IconButton
onClick={() => {
setSelectedIssueGroup(issueGroup)
modalPropsUpdate.onOpen()
}}
/>
{ console.log("update knows what the issuegroup is", selectedIssueGroup)}
<Modal {...modalPropsUpdate } title="Update Issue Group" >
<Form {...form} onSubmit={handleSubmitUpdate} >
<Stack>
<Input name="title" label="Title" />
<ButtonGroup>
<Button onClick={props.onClose}>Cancel</Button>
<Button
type="submit"
>
Save changes
</Button>
</ButtonGroup>
</Stack>
</Form>
</Modal>
<IconButton
onClick={() => onDeleteIssueGroup(issueGroup.id)}
/>
</ButtonGroup>
From the code you posted for AdminUpdateIssueGroupForm, there isn't a variable defined as issueGroup here:
export function AdminUpdateIssueGroupForm(props: Props) {
const router = useRouter()
const [updateIssueGroup] = useUpdateIssueGroupMutation()
const { data: issueGroups, refetch: refetchAllIssueGroups } = useAllIssueGroupsQuery()
const form = useForm({ schema: IssueGroupSchema })
const handleSubmit = async(data:IssueGroupInput) => {
// await form.triggerValidation()
console.log("made it to the handle submit before success handler")
return await form.handler(() => updateIssueGroup({
variables: {
// ========== issueGroup has not yet been defined ===========
// ========== Do you need to pull anything out of the 'data' variable to get the right issueGroup variable? ===========
id: issueGroup.id,
If you are looking to update the data from a single issueGroup instance that derives from the issueGroups variable, it looks like it would derive from the data variable you defined at the handleSubmit function.
A typical pattern for a list in react with an onClick action on an item where you want to use the id is:
list.map(el => (
<div key={el.id} onclick={() => clickHandler(el.id)>
... the rest of your layout
</div>
)
where the clickHandler function is whatever function you want to run on click. That function should accept the object's id as a parameter. That function can in turn call the form's submit handler should you be using forms.
I think your attempt 2 will work fine if you do this
You are setting a state and passing it to component like
<AdminUpdateIssueGroupForm onClose={modalPropsUpdate.onClose} issueGroup={selectedIssueGroup} />
and getting the props in AdminUpdateIssueGroupForm like ,
function AdminUpdateIssueGroupForm(props: Props) {
so you should access issueGroup.id like this in form handler,
return await form.handler(() => updateIssueGroup({ variables: { id: props.issueGroup.id, data: { ...data } } }), {
Note: In your last attempt also, you can access props.issueGroup.id
UPDATE:
for the selectedIssueGroup issue, i believe you are talking about the interface Props here.
In that interface you should provide a type to issueGroup not a state variable.So try this,
declare a type before usestate as
type IssueGroupType = Omit<
IssueGroupGQLType,
"createdAt" | "updatedAt" | "issues"
> | null
and use it in both state and your interface
State:
const [selectedIssueGroup, setSelectedIssueGroup] = React.useState<IssueGroupType>(null)
interface:
interface Props {
onClose: () => void
issueGroup: IssueGroupType
}
You need to create a separate handler for the Edit button and set some state, then just pass that state in editFormModal component like the code is given below. To test the code, you can click Here
export default function IssueGroups() {
const toast = useToast()
const [isOpen, setIsOpen] = useState(false);
const [loadGreeting, { error, called, loading, data }] = useLazyQuery(
GET_ALL_ISSUE_GROUPS,
{
fetchPolicy: 'no-cache',
}
);
const [modalValue, setModalValue] = useState({});
function handleEditClick(data) {
setModalValue(data);
setIsOpen(true);
}
React.useEffect(() => {
loadGreeting();
}, []);
const setUpdatedData = (obj) => {
toast({
title: 'Update Success.',
description: "Issue group is updated successfuly",
status: 'success',
duration: 5000,
isClosable: true,
})
loadGreeting()
setIsOpen(false);
};
if (error) return <p>Error : {error.message}</p>;
return (
<>
<Modal
onClose={() => {
setIsOpen(false);
}}
isOpen={isOpen}
size="full"
>
<AdminUpdateIssueGroupForm
IssueGroupData={modalValue}
setUpdatedData={setUpdatedData}
/>
</Modal>
{!loading && <TableContainer>
<Table variant="simple">
{/* <TableCaption>Imperial to metric conversion factors</TableCaption> */}
<Thead>
<Tr>
<Th>Title</Th>
<Th>Description</Th>
<Th>Date</Th>
<Th>Action</Th>
</Tr>
</Thead>
<Tbody>
{data &&
data.IssueGroup.map((group) => (
<Tr key={group.id}>
<Td>{group.title}</Td>
<Td>{group.description}</Td>
<Td>{group.createdAt}</Td>
<Td>
<Button
onClick={() => {
handleEditClick(group);
}}
>
{' '}
Edit{' '}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>}
</>
);
}
I'm doing a project with React and typescript, but I'm not able to pass properties to a React component, this property is an array of custom elements, this element has 3 pieces of information, name, age and profession. Currently I just want the component to wrap the array and for each item, show an h1 with the name. This is the code:
import { ChangeEvent, useState } from "react"
import { Button } from "../elements/Button/Button"
import { ContainerStyles } from "../elements/ContainerStyles/ContainerStyles"
import { GlobalStyles } from "../elements/GlobalStyles/GlobalStyles"
import { Input } from "../elements/Input/Input"
import { Elements } from "../elements/Elements/Elements"
import axios from "axios";
export type personType = {
name: string,
age: number,
profession: string
}
export const Home = () => {
const [InputName, setName] = useState<string>();
const [InputAge, setInputAge] = useState<string>();
const [Inputprofession, setInputProfession] = useState<string>();
const [personNameToSearch, setPersonNameToSearch] = useState<string>();
const [person, setPerson] = useState<Array<personType>>([]);
const getName = (e:ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}
const getAge = (e:ChangeEvent<HTMLInputElement>) => {
setInputAge(e.target.value)
}
const getProfession = (e:ChangeEvent<HTMLInputElement>) => {
setInputProfession(e.target.value)
}
const AddPerson = () => { // essa função está sendo ativada, mas está caindo no catch
axios.post('http://localhost:3333/person/', {
name: InputName,
idade: InputAge,
profissao: Inputprofession
}).then((response) => {
console.log(response);
}).catch((response) => {
const message = response.message;
if (message == 'Request failed with status code 500') {
console.log('Ha informações faltando ou uma informação foi dada de forma incorreta')
}
});
}
const GetPersonsInfo = async () => {
axios.get(`http://localhost:3333/person/${personNameToSearch}`, {}).then(
function (response) {
const name = response.data.name
const idade = response.data.idade
const profissao = response.data.profissao
setPerson([...person, {
name: name,
age: idade,
profession: profissao
}])
}
);
console.log(person)
}
const getPersonName = (e:ChangeEvent<HTMLInputElement>) => {
setPersonNameToSearch(e.target.value)
}
return(
<>
<GlobalStyles/>
<ContainerStyles>
<Input onChange={(e) => getName(e)} placeholder={"Nome:"}/>
<Input onChange={(e) => getAge(e)} placeholder={"Idade:"}/>
<Input onChange={(e) => getProfession(e)} placeholder={"Profissão:"}/>
<Button onClick={() => AddPerson()}>Adicionar usuário</Button>
</ContainerStyles>
<ContainerStyles>
<Input onChange={(e) => getPersonName(e)}/>
<Button onClick={() => GetPersonsInfo()}>Mostrar Usuários</Button>
</ContainerStyles>
</>
)
}
and:
import React from "react";
import { personType } from "../../pages/Home";
export const Elements = ( persons:Array<personType> ) => {
return (
<>
{persons.map((person: personType) => <h1 {person.name}></h1>)};
</>
);
};
You don't need to pass the name to an h1 as a prop. Just use <h1>{person.name}</h1> instead of what you have there.
You also are using the wrong format for props. Wrap persons:Array<personType> in file 2 in curly brackets.
Lastly, you will need to make sure you are rendering the elements component. If you are not rendering it, it doesn't do anything. When you render it, do it like this:
// replace personsArray with the name of your array
<Elements persons={personsArray}/>
I'm pretty new to Jest and testing, so I'm making an app using React, React Testing Library, and Jest to improve my skills.
One of my tests is failing, and I can't figure out why. Here is the code from my test:
import { render, screen, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// using UrlShortener since the state has been lifted up for UrlList
import UrlShortener from '../../pages/UrlShortener/UrlShortener'
...
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
userEvent.click(submitBtn);
const listButton = screen.getByText('Link History');
userEvent.click(listButton);
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
The thing that's confusing me is that I can see that the list is 4 elements long in the log from the failing test, but for some reason it's not getting picked up in the expect() function. Here's what the log is giving me (it clearly shows 4 elements in the list):
expect(received).toHaveLength(expected)
Expected length: 4
Received length: 3
Received array: [<p>Visits: 2</p>, <p>Visits: 1</p>, <p>Visits: 5</p>]
...
<div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/123"
>
test1
</a>
</p>
<p>
Visits:
2
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/456"
>
test2
</a>
</p>
<p>
Visits:
1
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/789"
>
test3
</a>
</p>
<p>
Visits:
5
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/shorten/123"
>
test4
</a>
</p>
<p>
Visits:
9
</p>
</div>
</div>
How is it possible that the DOM is behaving as expected in the log, but is failing in the actual test?
Update:
I'm adding more information so it's obvious what I'm doing. Basically, I've lifted state up from a child component (UrlList) to the parent (UrlShortener) so that I could pass a state updater function down to a sibling (UrlBar). The UrlShortener makes an axios call to the backend, then passes down a list of URLs to the UrlList component. When you click the submit button in the UrlBar component, it re-runs the axios call and updates the list with the new URL added.
Parent component:
import { useEffect, useState } from 'react';
import { SectionPage, BackButton, PageTitle } from './style';
import axios from 'axios';
import UrlBar from '../../components/UrlBar/UrlBar';
import UrlList from '../../components/UrlList/UrlList';
import { Url } from '../../types/types';
const UrlShortener = () => {
const [urls, setUrls] = useState<Url[] | []>([]);
const getUrls = () => {
axios
.get('https://fullstack-demos.herokuapp.com/shorten/urls/all')
.then((res) => setUrls(res.data));
};
useEffect(() => {
getUrls();
}, []);
return (
<SectionPage>
<BackButton href='/'>Go Back</BackButton>
<PageTitle>URL Shortener</PageTitle>
<UrlBar getUrls={getUrls} />
<UrlList urls={urls} />
</SectionPage>
);
};
export default UrlShortener;
Children:
import React, { useState } from 'react';
import {
ComponentWrapper,
Subtitle,
Triangle,
LinksContainer,
LinkGroup,
} from './style';
import { Url } from '../../types/types';
interface IProps {
urls: Url[] | [];
}
const UrlList: React.FC<IProps> = ({ urls }) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prevState) => !prevState);
};
return (
<ComponentWrapper>
<Subtitle onClick={handleClick}>
Link History <Triangle>{open ? '▼' : '▲'}</Triangle>
</Subtitle>
<LinksContainer>
<div>
{open &&
urls.map(({ urlId, shortUrl, urlName, visits }: Url) => (
<LinkGroup key={urlId}>
<p>
<a href={shortUrl}>{urlName}</a>
</p>
<p>Visits: {visits}</p>
</LinkGroup>
))}
</div>
</LinksContainer>
</ComponentWrapper>
);
};
export default UrlList;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { UrlInput, NameInput, UrlButton } from './style';
import { validateUrl } from '../../utils/utils';
interface IProps {
getUrls: () => void;
}
const UrlBar: React.FC<IProps> = ({ getUrls }) => {
const [urlInput, setUrlInput] = useState('');
const [nameInput, setNameInput] = useState('');
const [error, setError] = useState<boolean | string>(false);
useEffect(() => {
// Cleanup fixes React testing error: "Can't perform a React state update on an unmounted component"
return () => {
setUrlInput('');
};
}, []);
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrlInput(e.target.value);
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNameInput(e.target.value);
};
const handleSubmit = async (e: React.SyntheticEvent) => {
e.preventDefault();
if (!nameInput) {
setError('Please name your URL');
} else if (!validateUrl(urlInput)) {
setError('Invalid Input');
} else {
setError(false);
await axios.post('https://fullstack-demos.herokuapp.com/shorten', {
longUrl: urlInput,
urlName: nameInput,
});
setUrlInput('');
setNameInput('');
getUrls();
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<NameInput
type='text'
name='nameInput'
id='nameInput'
placeholder='Name your URL...'
maxLength={20}
onChange={handleNameChange}
value={nameInput}
/>
<UrlInput
type='text'
name='urlInput'
id='urlInput'
placeholder='Enter URL here...'
onChange={handleUrlChange}
value={urlInput}
/>
<UrlButton name='button' type='submit'>
Shorten!
</UrlButton>
{error && <label htmlFor='urlInput'>{error}</label>}
</form>
</div>
);
};
export default UrlBar;
So after fighting to get my tests to pass for another component, I finally figured out how to get this one to pass. Apparently I just needed to add a few more waitFor() and await statements to catch some of the async stuff happening in my component. I'd be lying if I said I understand why this fixes my problem, but now I know that if my tests are failing even though I can see the right results in the JEST DOM, it probably has to do with missing waitFor / awaits.
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
await waitFor(() => userEvent.click(submitBtn));
const listButton = await screen.findByText('Link History');
await waitFor(() => userEvent.click(listButton));
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
});
I have a simple app, sorta for chat purpuses. I fetch data from static file in json format. So this app shows all the messages from that file but also I want to edit the messeges, delete them and add via local storage. For that I used useEffect, but after refresh all the changes I do disappear.
This is my component:
export const WorkChat = (props) => {
const [messageValue, setMessageValue] = useState('');
const [edit, setEdit] = useState(null);
const [editmessageValue, setMessageEditValue] = useState('')
const submitMessage = () => {
const newMessage = {
id: Math.floor(Math.random() * 10000),
message: messageValue
}
props.addMessage(newMessage);
setMessageValue('')
}
const removeMsg = (id) => {
props.deleteMessage(id)
}
const goToEditMode = (message) => {
setEdit(message.id);
setMessageEditValue(message.message)
}
const saveChanges = (id) => {
const newMessagesArray = props.messages.map(m => {
if(m.id === id){
m.message = editmessageValue
}
return m
})
props.updateMessage(newMessagesArray);
setEdit(null)
}
useEffect(()=> {
let data = localStorage.getItem('work-messages');
if(data){
props.setMessages(JSON.parse(data))
}
}, []);
useEffect(()=> {
localStorage.setItem('work-messages', JSON.stringify(props.messages))
},[props.messages])
return (
<div className={s.workChatContainer}>
<input className={s.workInput} placeholder='Enter work message...' onChange={(e)=> setMessageValue(e.target.value)} value={messageValue}/>
<button className={`${s.btn} ${s.sendBtn}`} onClick={()=>submitMessage()}><SendIcon style={{fontSize: 20}}/></button>
<div>
{props.messages.map(m => (
<div key={m.id} className={s.messages}>
{edit !== m.id ? <div>
<span className={s.message}>{m.message}</span>
<button className={`${s.btn} ${s.deleteBtn}`} onClick={()=> removeMsg(m.id)}><DeleteOutlineIcon style={{fontSize: 15}}/></button>
<button className={`${s.btn} ${s.editBtn}`} onClick={()=> goToEditMode(m)}><EditIcon style={{fontSize: 15}}/></button>
</div>
:
<form>
<input className={s.editInput} value={editmessageValue} onChange={(e)=> setMessageEditValue(e.target.value)}/>
<button className={`${s.btn} ${s.saveBtn}`} onClick={()=> saveChanges(m.id)}><BeenhereIcon style={{fontSize: 15}}/></button>
</form>
}
</div>
))}
</div>
</div>
)
}
Just in case, this is my container component:
import { connect } from "react-redux"
import { setFloodMessagesAC, addFloodMessageAC, deleteFloodMessageAC, upadateMessageAC } from "../../redux/flood-reducer"
import { FloodChat } from "./FloodChat"
import { useEffect } from 'react'
import data from '../../StaticState/dataForFlood.json'
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)
Why useEffect doesn't work? It seems to me like it should, but it doesnt.
I figured it out. Since I use data from static file, I need to implement functions that get/set data from/to local storage right where I import it which is container component. Once I put those useEffect functions in container component it works perfectly well.
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
useEffect(()=> {
let data = JSON.parse(localStorage.getItem('flood-messages'));
if(data){
props.setFloodMessages(data)
}
console.log('get')
}, [])
useEffect(() => {
localStorage.setItem('flood-messages', JSON.stringify(props.messages));
console.log('set')
}, [props.messages]);
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)
We are using react-select and fetching the items as the user types. I am not able to make it work with react-apollo.
Can someone help me provide a guideline?
Here is my unsuccessful attempt:
class PatientSearchByPhone extends Component {
updateProp = mobile => {
if (mobile.length < 10) return;
this.props.data.refetch({ input: { mobile } });
};
render() {
console.log(this.props.data);
return <AsyncSelect cacheOptions loadOptions={this.updateProp} />;
}
}
const FETCH_PATIENT = gql`
query Patient($input: PatientSearchInput) {
getPatients(input: $input) {
id
first_name
}
}
`;
export default graphql(FETCH_PATIENT, {
options: ({ mobile }) => ({ variables: { input: { mobile } } })
})(PatientSearchByPhone);
Versions:
"react-apollo": "^2.1.11",
"react-select": "^2.1.0"
Thanks for your time.
I got an e-mail asking a response to this question. It reminds me of this XKCD comics:
I do not recall the exact solution I implemented, so I setup a complete example for this.
This app (code snippet below) kickstarts searching as soon as you type 4 characters or more in the input box (You are expected to type artist's name. Try vinci?). Here is the code:
import React, { useState } from "react";
import "./App.css";
import AsyncSelect from "react-select/async";
import ApolloClient, { gql } from "apollo-boost";
const client = new ApolloClient({
uri: "https://metaphysics-production.artsy.net"
});
const fetchArtists = async (input: string, cb: any) => {
if (input && input.trim().length < 4) {
return [];
}
const res = await client.query({
query: gql`
query {
match_artist(term: "${input}") {
name
imageUrl
}
}
`
});
if (res.data && res.data.match_artist) {
return res.data.match_artist.map(
(a: { name: string; imageUrl: string }) => ({
label: a.name,
value: a.imageUrl
})
);
}
return [];
};
const App: React.FC = () => {
const [artist, setArtist] = useState({
label: "No Name",
value: "https://dummyimage.com/200x200/000/fff&text=No+Artist"
});
return (
<div className="App">
<header className="App-header">
<h4>Search artists and their image (type 4 char or more)</h4>
<AsyncSelect
loadOptions={fetchArtists}
onChange={(opt: any) => setArtist(opt)}
placeholder="Search an Artist"
className="select"
/>
<div>
<img alt={artist.label} src={artist.value} className="aimage" />
</div>
</header>
</div>
);
};
export default App;
You can clone https://github.com/naishe/react-select-apollo it is a working example. I have deployed the app here: https://apollo-select.naishe.in/, may be play a little?
The other option is to execute the graphql query manually using the client that is exposed by wrapping the base component with withApollo.
In the example below, we have,
BaseComponnent which renders the AsyncSelect react-select component
loadOptionsIndexes which executes the async graphql fetch via the client
BaseComponent.propTypes describes the required client prop
withApollo wraps the base component to give us the actual component we'll use elsewhere in the react app.
const BaseComponent = (props) => {
const loadOptionsIndexes = (inputValue) => {
let graphqlQueryExpression = {
query: QUERY_INDEXES,
variables: {
name: inputValue
}
}
const transformDataIntoValueLabel = (data) => {
return data.indexes.indexes.map(ix => { return { value: ix.id, label: ix.name }})
}
return new Promise(resolve => {
props.client.query(graphqlQueryExpression).then(response => {
resolve(transformDataIntoValueLabel(response.data))
})
});
}
return (
<>
<div className="chart-buttons-default">
<div className="select-index-input" style={{width: 400, display: "inline-block"}}>
<AsyncSelect
isMulti={true}
cacheOptions={true}
defaultOptions={true}
loadOptions={loadOptionsIndexes} />
</div>
</div>
</>
)
}
BaseComponent.propTypes = {
client: PropTypes.any,
}
const ComplementComponent = withApollo(BaseComponent);
Sorry if the example is a little off - copy and pasted what I had working rather than moving on without giving back.