QueryRenderer doesn't update after a mutation is committed - javascript

I am very new to relay and GraphQL and looking to emulate the behaviour of after adding a mutation, my component updates with the new mutation. For example, when I add an animal, it then shows up on my animal list.
I have read a bit about manually updating the store via optimistic updates, fetch container etc, however wondering if this behaviour can occur by using just a QueryRenderer HOC and a simple mutation.
The mutation works successfully, however I can only see the update after I refresh that page.
This is the code for the root query render:
export const Customer = ({ userId }) => {
return (
<QueryRenderer
environment={environment}
query={query}
variables={{ userId }}
render={({ error, props }) => {
//handle errors and loading
const user = props.customers_connection.edges[0].node
return (
<div className="flex justify-between items-center">
<h1 className="text-xl">Pets</h1>
<AddPetForm />
<PetList user={user} />
</div>
)
}}
/>
)
}
const query = graphql`
query CustomerQuery($userId: bigint) {
customers_connection(where: { userId: { _eq: $userId } }) {
edges {
node {
id
userId
pets {
id
animal {
...Animal_animal
}
}
}
}
}
}
`
Here is the Animal component that lives in the PetList component. The PetList component is the one I am expect to re-render, to include a new Animal component with the animal created with the mutation.
const Animal = ({ animal }) => {
return (
<li>
<Summary animal={animal} />
</li>
)
}
export default createFragmentContainer(Animal, {
animal: graphql`
fragment Animal_animal on animals {
name
category
imageUrl
weight
type
}
`
})
Here is the AddPetForm component:
const AddPetForm = () => {
const { hide } = useModal()
return (
<Modal>
<QueryRenderer
environment={environment}
query={query}
variables={{}}
render={({ error, props }) => {
//handle errors and loading
return (
<Form
hide={hide}
//pass down props from query
/>
)
}}
/>
</Modal>
)
}
const query = graphql`
query AddPetFormQuery {
enum_animal_category_connection {
edges {
node {
id
value
}
}
}
//other enums to use in the form
}
`
And the code for the mutation (this lives in the AddPetForm component):
const Form = ({ hide, ...other props }) => {
const { handleSubmit, register, errors, } = useForm({ defaultValues, resolver })
const [mutationIsInFlight, setMutationIsInFlight] = useState(false)
const [mutationHasError, setMutationHasError] = useState(false)
const onCompleted = (response, error) => {
setMutationIsInFlight(false)
if (error) onError(error)
else showSuccessNotification()
}
const onError = error => {
setMutationIsInFlight(false)
setMutationHasError(true)
}
const onSubmit = animal => {
setMutationIsInFlight(true)
setMutationHasError(false)
//assume animal has already been added
addAnimalAsPet({ animalId: animal.animalId, userId, onCompleted, onError })
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-wrap">
// all of the inputs
<div className="mt-3 w-full">
<PrimaryButton type="submit" fullWidth={true} loading={mutationIsInFlight}>
Save
</PrimaryButton>
</div>
{mutationHasError && <p className="text-red-700 my-3">An error has occurred. Please try again.</p>}
</form>
)
}
And the mutation code:
const mutation = graphql`
mutation addAnimalAsPet Mutation($animalId: Int, $userId: Int) {
insert_pets_one(object: { animalId: $animalId, userId: $userId }) {
id
}
}
`
export const addAnimalAsPet = ({ userId, animalId, onCompleted, onError }) => {
const variables = { userId, animalId }
commitMutation(environment, {
mutation,
variables,
onCompleted,
onError
})
}
Is there something obvious I am doing wrong? As you can see, there is a nested QueryRenderer HOC, is this breaking the automatic update somehow?
I have not used a subscription as I do not need real time event listening. This data would only be updated when the user adds a new pet, and this doesn't occur very frequently. And I haven't used the optimistic updates/response as there isn't really a need to show the user the update is successful before waiting for the response.
From the docs it looks like there is a way to do this using a Refetch Container, however will have to refactor my code to use a fragment.

The suggestion by xadm worked like a charm. There is a retry function that is a property of the object in the QueryRenderer render prop.
Here is a working example:
<QueryRenderer
environment={environment}
query={query}
variables={{ userId }}
render={({ error, props, retry }) => {
//handle errors and loading
const user = props.customers_connection.edges[0].node
return (
<div className="flex justify-between items-center">
<h1 className="text-xl">Pets</h1>
<AddPetForm refreshCustomerQuery={retry} />
<PetList user={user} />
</div>
)
}}
/>
I then simply call the refreshCustomerQuery function just after a successful mutation.
const onCompleted = (response, error) => {
setMutationIsInFlight(false)
if (error) onError(error)
else {
showSuccessNotification()
refreshCustomerQuery()
}
}

Related

Redux store is updated but view is not

I have the parent Posts.js component which map every object in posts array. In this function I try to filter all notes have same post_id as id of the current mapped post object. All stored in filteredNotes variable. Then I pass it to each child. Now the issue. When I want to add new note in specific post, the view doesn't update (new note was not added to the list) although the database and redux store has been updated successfully.
But when I try to remove that filter function, everything works just fine so I guess the main problem is there. Any idea how to fix this? Thanks
Posts.js
const posts = useSelector((state) => state.post.posts);
const notes = useSelector((state) => state.notes.notes);
useEffect(() => {
dispatch(getPosts());
dispatch(getNotes());
}, []);
const addNoteHandle = (val) => {
dispatch(addNote({new_note: val}));
}
return (
<div className="post__page">
<div className="post__list">
{posts.map((data) => {
let filteredNotes = notes.filter((i) => i.post_id === data.id);
return <Post data={data} notes={filteredNotes} />;
})}
</div>
<PostForm addNewNote={addNoteHandle} />
</div>
);
Post.js
export const Post = ({ data, notes }) => {
return (
<div className="post__item">
<div className="post__title">{data.title}</div>
<div className="post__note">
{notes.map(note => <div>{note.text}</div>)}
</div>
</div>
);
};
NoteForm.js
const NoteForm = ({ addNewNote }) => {
const [text, setText] = useState("");
return (
<div>
<Input value={text} onChange={(e) => setText(e.target.value)} />
<Button type="primary" onClick={() => addNewNote(text)} >
<SendOutlined />
</Button>
</div>
);
};
Action
export const addNote = ({ new_note }) => async (dispatch) => {
try {
const res = await axios.post("http://localhost:9000/api/note", new_note);
dispatch({ type: ADD_NOTE, payload: res.data });
} catch (err) {
dispatch({ type: NOTE_FAIL });
}
};
Reducer
case ADD_NOTE:
return {
...state,
notes: [...state.notes, payload]
};
use useSelector to get the component value from redux store. for some reason hook setText will not work to update the page component. I had a similar problem and could not find any solution. This code may help:
let text ='';
text = useSelector((state) =>
state.yourReducer.text);
Now show your text wherever you want
this will fix the issue until you find real solution

how to implement addItem using react-redux

I've implemented user list and can delete users dispatching action deleteUser().
Now I add user but once I click add button the data is not mapped in the list.
this is a reducer:
case ADD_USERS:
const newId = state.users[state.users.length-1] + 1
return {
...state,
users: [
...state.users,
{
id: newId,
name: action.payload
}
],
loading: false
}
initial state consists of 2 objects and loading key.
The action function is simple:
export function addUser (name) {
return {
type: ADD_USERS,
payload: name
}
and the component is there:
const mapStateToProps = (state) => ({ users: state.users });
const mapDispatchToProps = (dispatch) => {
return {
deleteUser: id => {
dispatch(deleteUser(id))
},
addUser: name => {
dispatch(addUsers(name))
}
}
};
const Users = (props) => {
const { users } = props.users;
useEffect(() => {
getUsers();
}, []);
return (
<>
<input type='text' placeholder='name..'/>
<button onClick={() => props.addUser(name)}>add</button>
<h2>Users</h2>
{users.map((user) => {
return (
<div className="d-flex justify-content-between align-items-center mb-1">
<li>{user.name}</li>
<button onClick={() => props.deleteUser(user.id)}>x</button>
</div>
);
})}
</>
);
};
}
I consider getUsers don't work or I can be wrong. cause I map state to props and display the data inside {user.name}
I think it should work same with getUsers()
Maybe this is not the only one issue, but at least this looks strange to me:
const { users } = props.users;
Because, with the line above you are creating a constant with value from props.users.users. You have not shown how you use the Users component and what it gets from outside, but this looks at least strange to me.
<button onClick={() => props.addUser(name)}>add</button>
Your button calls addUser with a variable name, but that variable doesn't exist!
You need to change your input into a controlled component so that you can call addUser with the name from the input field.
const [name, setName] = useState("");
return (
<>
<input
type="text"
placeholder="name.."
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={() => props.addUser(name)}>add</button>
...

Losing router object on child component rerender

I have a parent component GoalList which maps to a child component:
{data.goals.map((item, index) => {
return (
<Link
href={{ pathname: "/goal", query: { id: item.id } }}
key={`goal-item-${index}`}
>
<a>
<li>
<div>{item.title}</div>
</li>
</a>
</Link>
);
})}
next/router's page:
import SingleGoal from "../components/SingleGoal";
const Single = () => {
return <SingleGoal />;
};
export default Single;
Child Component:
const SingleGoal = () => {
const [id, setId] = useState("");
const router = useRouter();
useEffect(() => {
if (router.query.id !== "") setId(router.query.id);
}, [router]);
const { loading, error, data } = useQuery(SINGLE_GOAL_QUERY, {
variables: { id: id },
});
if (loading) return <p>Loading...</p>;
if (error) return `Error! ${error.message}`;
return (
<div>
<h1>{data.goal.title}</h1>
<p>{data.goal.endDate}</p>
</div>
);
};
When I click on Link in the parent component, the item.id is properly transferred and the SINGLE_GOAL_QUERY executes correctly.
BUT, when I refresh the SingleGoal component, the router object takes a split second to populate, and I get a GraphQL warning:
[GraphQL error]: Message: Variable "$id" of required type "ID!" was not provided., Location: [object Object], Path: undefined
On a similar project I had previously given props to next/router's page component, but this no longer seems to work:
const Single = (props) => {
return <SingleGoal id={props.query.id} />;
};
How do I account for the delay in the router object? Is this a situation in which to use getInitialProps?
Thank you for any direction.
You can set the initial state inside your component with the router query id by reordering your hooks
const SingleGoal = () => {
const router = useRouter();
const [id, setId] = useState(router.query.id);
useEffect(() => {
if (router.query.id !== "") setId(router.query.id);
}, [router]);
const { loading, error, data } = useQuery(SINGLE_GOAL_QUERY, {
variables: { id: id },
});
if (loading) return <p>Loading...</p>;
if (error) return `Error! ${error.message}`;
return (
<div>
<h1>{data.goal.title}</h1>
<p>{data.goal.endDate}</p>
</div>
);
};
In this case, the secret to props being transferred through via the page was to enable getInitialProps via a custom _app.
Before:
const MyApp = ({ Component, apollo, pageProps }) => {
return (
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
);
};
After:
const MyApp = ({ Component, apollo, pageProps }) => {
return (
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
);
};
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {};
if (Component.getInitialProps) {
// calls page's `getInitialProps` and fills `appProps.pageProps`
pageProps = await Component.getInitialProps(ctx);
}
// exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
};
The only downfall now is that there is no more static page generation, and server-side-rendering is used on each request.

How to give child component control over react hook in root component

I have an application that adds GitHub users to a list. When I put input in the form, a user is returned and added to the list. I want the user to be added to the list only if I click on the user when it shows up after the resource request. Specifically, what I want is to have a click event in the child component trigger the root component’s triggering of the hook, to add the new element to the list.
Root component,
const App = () => {
const [cards, setCards] = useState([])
const addNewCard = cardInfo => {
console.log("addNewCard called ...")
setCards([cardInfo, ...cards])
}
return (
<div className="App">
<Form onSubmit={addNewCard}/>
<CardsList cards={cards} />
</div>
)
}
export default App;
Form component,
const Form = props => {
const [username, setUsername] = useState('');
const chooseUser = (event) => {
setUsername(event.target.value)
}
const handleSubmit = event => {
event.persist();
console.log("FETCHING ...")
fetch(`http://localhost:3666/api/users/${username}`, {
})
.then(checkStatus)
.then(data => data.json())
.then(resp => {
console.log("RESULT: ", resp)
props.onSubmit(resp)
setUsername('')
})
.catch(err => console.log(err))
}
const checkStatus = response => {
console.log(response.status)
const status = response.status
if (status >= 200 && status <= 399) return response
else console.log("No results ...")
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Gitbub username"
value={username}
required
onChange={chooseUser}
onKeyUp={debounce(handleSubmit, 1000)}
/>
<button type="submit">Add card</button>
</form>
)
}
export default Form;
List component,
const CardsList = props => {
return (
<div>
{props.cards.map(card => (
<Card key={card.html_url} {... card}
/>
))}
</div>
)
}
export default CardsList
and the Card Component,
const Card = props => {
const [selected, selectCard] = useState(false)
return (
<div style={{margin: '1em'}}>
<img alt="avatar" src={props.avatar_url} style={{width: '70px'}} />
<div>
<div style={{fontWeight: 'bold'}}><a href={props.html_url}>{props.name}</a></div>
<div>{props.blog}</div>
</div>
</div>
)
}
export default Card
Right now, my Form component has all the control. How can I give control over the addNewCard method in App to the Card child component?
Thanks a million in advance.
One solution might be to create a removeCard method in App which is fired if the click event you want controlling addNewCard doesn't happen.
// App.js
...
const removeCard = username => {
console.log("Tried to remove card ....", username)
setCards([...cards.filter(card => card.name != username)])
}
Then you pass both removeCard and addNewCard to CardList.
// App.js
...
<CardsList remove={removeCard} cards={cards} add={addNewCard}/>
Go ahead and pass those methods on to Card in CardsList. You will also want some prop on card assigned to a boolean, like, "selected".
// CardsList.js
return (
<div>
{props.cards.map(card => (
<Card key={card.html_url} {... card}
remove={handleClick}
add={props.add}
selected={false}
/>
))}
</div>
Set up your hook and click event in the child Card component,
// Card.js
...
const [selected, selectCard] = useState(false)
...
and configure your events to trigger the hook and use the state.
// Card.js
...
return (
<div style={{margin: '1em', opacity: selected ? '1' : '0.5'}}
onMouseLeave={() => selected ? null : props.remove(props.name)}
onClick={() => selectCard(true)}
>
...
This doesn't really shift control of addNewCard from Form to Card, but it ultimately forces the UI to follow the state of the Card component.

Update apollo client with response of mutation

I have a login component that when submitted, calls a login mutation and retrieves the user data.
When the user data is returned, I want to set the apollo client state but I am confused by the docs on how to do so:
My component looks as so:
const LOGIN = gql`
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password) {
username
fullName
}
}
`
const Login = () => {
const onSubmit = (data, login) => {
login({ variables: data })
.then(response => console.log("response", response))
.catch(err => console.log("err", err))
}
return (
<Mutation
mutation={LOGIN}
update={(cache, { data: { login } }) => {
}}
>
{(login, data) => {
return (
<Fragment>
{data.loading ? (
<Spinner />
) : (
<Form buttonLabel="Submit" fields={loginForm} onChange={() => {}} onSubmit={e => onSubmit(e, login)} />
)}
{data.error ? <div>Incorrect username or password</div> : null}
</Fragment>
)
}}
</Mutation>
)
}
As you can see, I have the update prop in my mutation which receives the login param and has the user data which I want to set in the apollo client's state.
The example here writes the response to the cache cache.writeQuery but I am unsure if this is what I want to do. Would I not want to write to client (as opposed to cache) like they do in this example where they update the local data?
The update property of mutation only seems to receive the cache param so I'm not sure if there is any way to access client as opposed to cache.
How do I go about updating my apollo client state with the response of my mutation in the update property of mutate?
EDIT: my client:
const cache = new InMemoryCache()
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
clientState: {
defaults: {
locale: "en-GB",
agent: null /* <--- update agent */
},
typeDefs: `
enum Locale {
en-GB
fr-FR
nl-NL
}
type Query {
locale: Locale
}
`
},
cache
})
Using Context API
If you have wrapped your component somewhere higher up in the hierarchy with ApolloProvider
Using ApolloConsumer
const Login = () => {
const onSubmit = async (data, login, client) => {
const response = await login({ variables: data });
if (response) {
client.whatever = response;
}
};
return (
<ApolloConsumer>
{client => (
<Mutation mutation={LOGIN}>
{login => <Form onSubmit={e => onSubmit(e, login, client)} />}
</Mutation>
)}
</ApolloConsumer>
);
};
Using withApollo
const Login = client => {
const onSubmit = async (data, login) => {
const response = await login({ variables: data });
if (response) {
client.whatever = response;
}
};
return (
<Mutation mutation={LOGIN}>
{login => <Form onSubmit={e => onSubmit(e, login)} />}
</Mutation>
);
};
withApollo(Login);
Without Context API
import { client } from "wherever-you-made-your-client";
and just reference it there

Categories