Great job đź‘Ť A lot of good examples.
But I have some ideas to share. Although the Apollo team suggests using mutation directly in the component IMO in most cases it leads to overcomplicated components. So I came up to the solution where I mix apollo client with redux-saga.
In that case, I separate visual logic from business logic. My component doesn’t really care about the state updates, cache refreshing, and some post-operations.
Let’s take an example with a list.
export default class PostsList extends Component<IProps> {
componentDidMount(): void {
this.props.loadPosts();
} render() {
const { posts, loading } = this.props;
if (loading) return <Loading /> return (
<div>
{posts.map((post: Post) => (
<div>
<h4>{post.title}</h4>
<Button onClick={() => this.props.deletePost({id: post.id})}>
Delete
</Button>
</div>
))}
</div>
);
}
}
My component doesn’t really care what happens in deletePost
. It doesn’t know whether some graphql or REST call will happen next, or maybe some in memory deletion — it does not matter for the PostsList
. It doesn’t care about state updates and data refreshing. So the saga for that component would look like
function* loadPosts(action: ILoadPostsAction) {
const result = yield apolloClient.query({
query: loadPosts,
variables: { ... },
fetchPolicy: "no-cache"
});
yield put(savePosts(result.data.posts)); // save to state
};function* deletePost(action: IDeletePostAction) {
try {
yield apolloClient.mutate({
mutation: deletePost,
variables: { id: action.id }
});
} catch (errors) {
logger.error(errors);
return;
}
yield put(loadPosts());
// alternatively you can delete an item directly from state
// but I prefer this way, not a big issue with performance};
You’ll need to write a little bit more code but the resulted components are much more clear. Also, it’s much easier to test saga and component when they are separated.
I still widely use useQuery
in the components, but when it comes to mutations in 85% I used a mix of apollo and redux saga.
Thoughts?