Check out a free preview of the full Fullstack TypeScript (feat. GraphQL & Node.js) course
The "Client-Side Mutation" Lesson is part of the full, Fullstack TypeScript (feat. GraphQL & Node.js) course featured in this preview video. Here's what you'd learn in this lesson:
Mike walks through modifying the React component ComposePanel, how to fix the resulting UI bug, and answers a few student questions. The tweet data will persist, but Apollo will need to re-fetch the data when clicking the create tweet button.
Transcript from the "Client-Side Mutation" Lesson
[00:00:00]
>> In this next part, we're going to modify the React component called compose panel. That's where you're actually gonna type this tweet, and we'll use this mutation that we created in the previous step. So let's open our client side project, sub-project. And we're gonna go to ComposePanel.tsx. And in here, we're going to import some utilities, The apollo/client gql tag.
[00:00:30]
And something that will fail at first, the react hook for using a new Create Tweet mutation. So we're gonna add these up at the top. And we're gonna have to grab this mutation as well. Note the word mutation here, this is different from query. This is important in terms of wiring things up the right way.
[00:01:00]
Otherwise looks quite similar, and we'll look at that in more detail momentarily. All right, so note, we have these dollar sign things, CreateNewTweet, $userId and $body, with these dollar signs. So these are the variables coming into your mutation. And then you could have potentially more than one of these things that uses the variables, right?
[00:01:30]
So this is the idea of having a mutation and then variables, and then they can be doing more than one thing down here. Which is important if you wanted to sort of almost create a transaction of sorts, where many things are happening at the same time, you want them to be going on concurrently.
[00:01:51]
It's not strictly transaction in that it depends on your implementation and your resolver, whether you let it to either complete as one atomic operation or not. But in any case, it's sort of gonna be one promise from the standpoint of the consumer of the API. And this is a nice little aspect of the VS Code GraphQL plugin, you can hit Execute Mutation and it's gonna ask you for the variables, the value for user ID, the value for the other thing.
[00:02:21]
And then the response would show up over here, but I'm not gonna bother doing that. Here's that error from our server. Nice to be able to do that right from within your code. So great, we have the new operation in our UI. That means we have to CodeGen again, in the client project, and yarn codegen.
[00:02:44]
Show me some checkmarks. Great, at which point this, yep, red line went away, now it's just unused. And we can see here's the type. These are the variables that it takes in, looks like everything's flowing through as we would hope. So the next thing we'll do is delete the whole thing.
[00:03:03]
We're going to replace it with the hook equivalent. In fact, Copilot is correctly suggesting it to me. There it is, CreateNew Tweet. And then there's a second argument that we can capture here, which is error. And I believe there's a loading property here as well if you wanted a little spinner that would render while you're persisting it.
[00:03:27]
It's gonna happen so fast in our case that it's not even worth worrying about. So that's the first line, and then we can say, if( error ), this is on the website code if you wanna just copy paste it. Return, Return the error creating tweet, and then we'll have the error, something like that.
[00:03:57]
All right, so there's your hook, and we're leaving it with the same name. Should be okay. And we're gonna have to just change what's happening at the call site. So handleSubmit, this is actually what's wired up to the DOM event here, right? This is the form submit event.
[00:04:19]
This is our invocation of CreateNewTweet. This is where things are actually happening. In this case, we just Console-logged, and then this was like the placeholder implementation. We'd log and say, this is where we would do the thing, and then we clear the text area, so that the composed tweet goes away.
[00:04:39]
Well, in this case, what we're doing is gonna be async. So we're gonna need to change this a little bit. At the very least, we'll have to say .then, and then clear the input or the text area, something like that. Right, and let's bound to EsLint. The other thing is, we're not just passing in body here.
[00:05:06]
We're passing in something that this GraphQL react hook is designed to receive and that is a variables object. So there's this config object. For now, we're just using variables and we'll pass in userId. That's gonna be currentUser.Id, and then body is something that we're already getting here. Also, we have a dangling promise here which is bad Juju.
[00:05:32]
So we wanna add a catch block, Something like that. So just in summary, we created our new mutation. We replaced our placeholder, CreateNewTweet, function with the use of a hook. We're handling this error in the case if things go wrong. So this will happen after we attempt to persist things, maybe our server goes down to the back end, and this little text will show up, which is great.
[00:06:13]
And now, we're changing the way we're invoking this, we're not passing the body in directly. We're passing in this variables object, giving it the user ID and the body. Again in our schema, there's userId, there's body. So these are not local names, they are part of your public API.
[00:06:35]
And then we're clearing things out in the event things complete successfully, and we're erroring in the event that things go wrong. Let's try this out, let's see if it works. Yeah, so I got a question in chat, is error unknown? It should be, these days I should really make sure it's an unknown.
[00:07:00]
I wonder if, You know what, I would have to type it here. So there's a new TypeScript feature that makes it so all caught values, all values you're encountering catch blocks are unknowns. But this is literally the catch type of the promise API, so it doesn't really benefit from that.
[00:07:24]
However, you're welcome to type it as an unknown. In fact, I think it's wise to do so. Yeah, Mark, you're correct, I did suggest doing this in the Making TypeScript Stick course. But that's for try-catch, the sort of the fundamental control flow JavaScript concepts. In the Promise API, I do not believe that these new language features carry through it.
[00:07:52]
In fact, I'd be surprised if I hadn't already set things up that way. Let's see. Yep, use unknown and catch variable. So I already have this turned on, it's just, it's sort of the async version of it. If this were real try-catch, you would see it. But that's the right instinct, people who are thinking, make that an unknown, good, keep that.
[00:08:16]
All right, let's kick the tires, see if things work. So we should be able to, let's refresh, just make sure we get the latest code. And hello, this is a first tweet. By the way, this is from nine minutes ago, this is the stuff that we persisted through the dev tools.
[00:08:35]
So that's showing up. And I'm gonna hit Compose. Boom, and we see nothing, the text area gets cleared and we see nothing. And if we refresh, there it pops up. Less than a minute ago, this tweet was created. So what's happening here is we are successfully persisting the data.
[00:08:55]
We can see it, first off, when we reload, we see that the data comes back. In addition, if we look at our changes, we can see, this just happened, what's the current time? 37 minutes past the hour, that's probably this one here, this is my first tweet. So it's persisting, but what's happening is the state in my react app is not updating.
[00:09:17]
Now, normally you would do this through some state management library or you would use set state or something like that. There is another option in the Apollo world. And that is the idea of re-fetching the data associated with a particular query. And that can happen in sort of an action at a distance way.
[00:09:43]
So although we're in this composed panel, and something just happened, the data we want to effectively invalidate and refresh, that's in this Timeline.tsx file. It's this query here called GET_TIMELINE_TWEETS. And in terms of how these are organized, ComposePanel is a child component of Timeline, right? It's within Timeline.
[00:10:11]
So it's a child that wants to sort of tell its parent to reload. Many different ways to do this, but I wanna show you, this is a good opportunity for me to show you how you can do this just at the Apollo layer. So the way we're gonna fix this bug is adding a new option to the way we're invoking this hook.
[00:10:34]
And it's going to be refetchQueries, and we'll pass in an array containing GET_TIMELINE_TWEETS. So this is what's exported from Timeline. Remember, we're exporting all of our queries here. See, we're bringing it in up here. And we're just saying refetchQueries, GET_TIMELINE_TWEETS. So when this comes back, it'll say, hey, something just happened, and that something seems to feel that you could get something different.
[00:11:09]
So why don't you go ahead and refetch this? And behind the scenes, what's happening is a set state, and that's causing the timeline component to refetch and update accordingly. So I'm gonna save this real quick, and let's go ahead and test the UI one more time. I'm gonna refresh to make sure I get the latest stuff.
[00:11:34]
Does this automatically work? Will we see it right away? And we do. And in fact, interesting, the tweet count isn't changing here. If I refresh, it is changing. Actually, we could fix that as well. It's getting a little heavy handed here. We could refresh that GET_CURRENT_USER query as well if we wanted to.
[00:12:04]
Let's [LAUGH] do that. If you're feeling like this is starting to get out of hand, fair point. No more refetching. And we can hit this. Now, are we gonna see the tweets count go from three to four? Yes, I love boiling the ocean. It makes so much steam.
[00:12:43]
And five. Action at a distance is a little dangerous, because you have to think carefully about how you'd be able to debug this kind of thing. And what you don't want is a situation where someone's performing some interaction, and it's causing mysterious side effects to happen elsewhere in the app.
[00:13:05]
So use it like salt, a little bit goes a long way. But this is a nice, in the case of the timeline tweets, this is, I would say, a straightforward kind of thing to do. And it certainly beats the idea of trying to shove some client side representation of that data structure.
[00:13:22]
Which might not have a timestamp on it or an ID or whatnot, trying to shove that into an array and optimistically update the timeline. I would prefer just get the new data, right? Nicolas asked a good question in chat, maybe we need idempotency. So the idea of idempotency, [LAUGH] it's a very fancy word that means, Effectively performing an operation more than once with the same inputs will turn out to be a no op.
[00:14:06]
So a lot of this has to do with writing code in such a way where your code just wants to reach a given state. And if no work needs to be done in order to get into that state, no work will be done. This is how React does its rendering, for example.
[00:14:23]
In this case, I wouldn't argue that we're doing things in an idempotent way, because we are fetching a lot of data that we might already have, if that makes sense. So we are doing more work than we need to. Yeah, all right, from the classroom, Mark asks, can current user summary and the tweet list subscribe to changes somehow?
[00:14:54]
It feels like compose panel knows too much about other components. I could see how you would feel that way, right, the compose panel is reaching up to its parents query. And one could argue that that sort of breaks the separation of concerns. GraphQL does support the concept of subscriptions.
[00:15:19]
I'm just gonna kinda show you what that looks like. So just, yeah, here we go. You can create a type called Subscription, it's very similar to query and mutation, it's one of these special top level types. Here's the thing though. You know how GraphQL supports querying and mutation, but it's totally on us to implement the actual work that is done.
[00:15:47]
We're touching a DB, right? So the trick is not necessarily like is it possible to use subscriptions with GraphQL? Absolutely, it is possible. But you get into the same problem that other near real time apps run into. Which is, there's a significant overhead for each user that's connected to your app, right, be it through a WebSocket, or long polling, or anything like that.
[00:16:16]
It makes sense if you're building a chat app and you wanna know when new messages arrive. But if you do it for all of the data that's in the UI, it's just a lot, and it's expensive in terms of operating that system and making sure that it's performant.
[00:16:38]
So I would say, Yes, it's possible, be careful. The problem GraphQL solves in this space is not the hardest problem to solve in this space.. And that the hard problem is, how do you do this in an efficient way, and how do you use this near real time update sparingly in the right cases where it matters?
[00:17:00]
If you're building a stock ticker app or a currency trading app or an Ebay, and you wanna show the auction countdown. And you want that UI to update and for that to be definitely how much time is left, cuz someone's making a purchase decision based on that. That sounds like a valuable place to use this kind of thing, but I don't know if I would use it for a Twitter clone.
[00:17:26]
Nicholas, in class, points out that the client could also provide the count value that should be updated. So I assume what you mean is, at the time the compose panel submits the tweet, we could just say the count is incremented by one. Yes, of course, we could do that.
[00:17:47]
But I'm not sure that I would advocate for doing things that way, because what if the user has multiple devices, and they send a tweet from their phone and they have had this page open on their browser for a week? It's kinda just there, they keep it on the third monitor, and then I want the state to kinda come from the source of truth.
[00:18:09]
I don't want it sometimes to come from my back end, and sometimes to be coming from my front end. That can result in a confusing experience sometimes.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops