Tutorials on Optimistic Uis

Learn about Optimistic Uis from fellow newline community members!

  • React
  • Angular
  • Vue
  • Svelte
  • NextJS
  • Redux
  • Apollo
  • Storybook
  • D3
  • Testing Library
  • JavaScript
  • TypeScript
  • Node.js
  • Deno
  • Rust
  • Python
  • GraphQL
  • React
  • Angular
  • Vue
  • Svelte
  • NextJS
  • Redux
  • Apollo
  • Storybook
  • D3
  • Testing Library
  • JavaScript
  • TypeScript
  • Node.js
  • Deno
  • Rust
  • Python
  • GraphQL

Optimistic UIs with React, Apollo Client and TypeScript (Part III) - Handling Errors

Disclaimer - Please read the second part of this blog post here before proceeding. It walks through the steps taken to update a UI optimistically with Apollo Client. However, it does not discuss how to elegantly handle failed mutations, which, by default, automatically undo optimistic updates made to the UI. If sending a message within a messages client causes the UI to update optimistically with this message, then anytime the server encounters an error (e.g., network or GraphQL) while performing the mutation, the message instantly disappears from the UI. For the user to resend the message, they must retype the message in the input field and resend it. Another problem that arises from reverting the optimistic UI update is the loss of the original timestamp the message was sent at since Apollo Client automatically removes the optimistic data from the cache. In the final part of this tutorial series, we will take the messages client that has been built over the course of the past two parts of this tutorial series and implement a more convenient way for users to resend unsent messages (as a result of a network or GraphQL error). When an error occurs during the mutation, the UI should... By the end of this tutorial, you will have recreated a robust optimistic UI that is capable of resending unsent messages like the one found in Messages : To get started for this tutorial, download the project from the part-2 branch of the GitHub repository here and follow the directions in the README.md file to set up the project. One thing you will notice in the codebase is the template literals passed to the gql tags. Each template literal encloses a GraphQL query or mutation that is executed within a component of the application. Let's move these template literal tags out into a separate module and export each as a named export: ( src/graphql/fragments.ts ) Then, visit each component's source file, and anytime you come across one of these template literal tags, import its corresponding named export and replace it accordingly with the imported value. By refactoring these template literal tags, we can centralize all of the template literal tags in a single place. We import gql from the @apollo/client library just once within the entire application (in the src/graphql/fragments.ts file), and we can reuse these template literal tags anywhere in the application. To understand the general strategy we will take to bypass the UI's instantaneous undoing of the optimistic update, we must dive deep into how the Apollo Client handles an optimistic UI update within the in-memory cache. Disclaimer : At the time of this writing, the current version of the Apollo Client library ( @apollo/client ) is v3.5.8. If you are reading this several months/years after the original publication date, then the underlying architecture may have changed. In part 2 of this tutorial series , I mentioned that the Apollo Client creates and stores a separate, optimistic version of the message in the cache. Here, "optimistic version" refers to an optimistic data layer ( Layer layer) that Apollo Client creates on top of the Stump and Root layers of the cache. Each layer of the cache is responsible for managing its own data, whether it is data associated with an optimistic UI update ( Layer layer) or queries ( Root layer). Partitioning data this way makes it easy to identify which set of optimistic changes (made to the cache's data) to undo when the GraphQL API server returns the result of a mutation. When you inspect the optimistic layer via a JavaScript debugger in the developer tools, you will find that the layers reference each other via a parent property. The deeply nested Root layer holds all the data associated with queries (i.e., the messages seen in the messages client), the Stump layer holds no data and the optimistic layer ( Layer layer) holds all data associated with an optimistic UI update (i.e., the sent message). The Stump layer serves as a buffer (between the optimistic data layers and root data layer) that allows subsequent optimistic updates to invalidate the cached results of previous optimistic updates. With all the Layer layers sharing the Stump layer, all optimistic reads read through this layer. As a buffer, no data is ever written to this layer, and look up and merge calls skip over this layer and get forwarded directly to the Root layer. Note : For more information, please read the pull request that introduced the Stump layer to the cache. Whenever the mutate function is called, the Apollo Client checks if an optimisticResponse option is provided for the mutation. If so, then the Apollo Client marks the mutation as optimistic and wraps the optimistic write within a transaction . When performing this transaction , the Apollo Client adds a new optimistic data layer to the cache . Notice how there are twenty-one messages on the optimistic data layer (the twenty original messages queried from the GraphQL API server plus the message added via the optimistic update) and twenty messages on the root data layer (the twenty original messages queried from the GraphQL API server). Once the cache finishes updating with the new optimistic data layer, broadcastQueries gets called. All active queries listening to changes to the cache will update, which causes the UI to also update. Since broadcastQueries is an asynchronous operation, the UI may not immediately update with the optimistic data even if the debugger has moved on to the next breakpoint. By isolating all of the optimistic updates (carried out by this transaction) to this layer, the Apollo Client never merges the optimistic data with the cache's root-level data. This ensures that the optimistic updates can be easily undone, such as on a mutation error , by deleting the optimistic data layer and readjusting the remaining optimistic data layers (from other pending mutations) , all without ever touching the root-level data. If the mutation fails (or succeeds), then by calling broadcastQueries , the Apollo Client updates the active queries based on recent updates made to the cache, which no longer has the optimistic data layer for the addMessage mutation. This removes the sent message from the UI. Now that we know how the cache works, let's devise a solution that keeps the sent message shown even when the addMessage mutation fails. Given that the first broadcastQueries call updates the UI with the optimistic data (the sent message) and the last broadcastQueries call undoes updates to the UI that involve the optimistic data, we need to add the sent message to the cache's root-level data at the moment the mutation fails between these two calls. This duplicate message will have an identifier of ERROR/<UUID> to... While both pieces of data will exist at the same time, only the message on the optimistic data layer, not the duplicate message on the root data layer, will be rendered. Only the message on the optimistic data layer existed within the cache at the time of the first broadcastQueries call. By adding it to the root data layer, the duplicate message will still exist by the time the last broadcastQueries call occurs. However, since the optimistic data layer gets removed just before this broadcastQueries call, the message on the optimistic data layer will no longer exist. Both messages contain the same text. Hence, nothing seems to change on the UI. The user never sees the optimistic data disappear. Both messages never get rendered together. For us to write the duplicate message to the cache, we must add an onError link to the Apollo Client's chain of link objects. The onError link listens for networking and GraphQL errors during a GraphQL operation (e.g., a query or mutation) and runs a callback upon encountering a networking/GraphQL error. Currently, the Apollo Client already uses the HTTPLink link to send a GraphQL operation to a GraphQL API server that performs it and responds back with either a result or error. Since each link represents a piece of logic to apply to a GraphQL operation, we must connect this link with the onError link to create a single link chain. Let's do this with the from method, which additively composes the links. As a terminating link, the HTTPLink link ends up being the last link in the chain. Defining the links in this order allows the server's response to bubble back up to the onError link. Within the callback passed to the onError link, we can check for any networking/GraphQL errors. If the response is successful, then the onError link simply ignores the response's data as it makes its way to the cache. Shortly after the first broadcastQueries call, the addMessage mutate function executes getObservableFromLink , which obtains the observable of the Apollo Client's link. Unlike promises, observables are lazily evaluated, support array-like methods (e.g., map and filter ) and can push multiple values. Then, the addMessage mutate function invokes the observable by subscribing to it. Essentially, invoking the observable of the HTTPLink link sends a request for this mutation to the GraphQL API server. Note : If you're unfamiliar with observables, you can learn more about them here . Developers who have worked with Angular and/or RxJS have likely previously come across observables. If the Apollo Client encounters a networking/GraphQL error, then the onError link's callback gets called. This callback logs the caught error and checks the name of the GraphQL operation. If the name of the GraphQL operation happens to be AddMessage , which corresponds to the AddMessage mutation, then the Apollo Client adds a message with the same text and sender as the originally sent message to the root data layer of the cache. We create this message based on the variable values provided to the mutation: text and userId . Note : We can pass true as the second argument to the readQuery method ( const { messages } = cache.readQuery({ query: GET_MESSAGES }) ) to include the optimistic data (i.e., the message on the optimistic data layer) in the list of queried messages. However, there are some caveats to this approach. if there are multiple ongoing AddMessage mutations, then the list will also include the optimistic data from those mutations. Therefore, filtering the messages by text and userId to find the optimistically created message is unreliable, especially if the user has just sent several, pending, consecutive messages with the same text. This makes it difficult to modify the optimistically created message's id to follow the ERROR/<UUID> format. Also, because there is a very small discrepancy between this message's timestamp and the timestamp recorded within the callback, the ordering of the messages is unaffected. Finally, the error callback of the observable receives the error , removes the optimistic data layer associated with the mutation and calls broadcastQueries to update the active queries. Since we added a copy of the message to the cache before the optimistic data layer was removed, the view will still display the message to the user. Within the src/index.tsx file, let's add the onError link to the Apollo Client's link chain, like so: ( src/index.tsx ) In the browser, wait for the application to fully refresh. Select a user. Once the application loads the messages, open the developer tools. Under the network tab of the developer tools, select the "Offline" option under the throttling dropdown to simulate zero internet connectivity. When you try to send a message, the message will remain on the UI even if the mutation fails. Now the user no longer has to re-type the message to send it. Plus, the timestamp at which it was originally sent will be preserved (more or less). Click here for a diagram that visually explains the solution. At this point, the sent messages and unsent messages look identical. Each one has a blue background with white text. To distinguish sent messages from unsent messages, let's add a button next to each unsent message that could not be sent as a result of a failed mutation. The button will be displayed as a red exclamation point icon. When clicked on, a dialog will pop open, and it will ask the user for confirmation to resend the message (at its original createdAt timestamp). Within the JSX of the <MessagesClient /> component, next to the <p /> element with the message's text, we need to check if the message's sender is the current user (since unsent messages displayed in the client belong to the current user) and the message's ID for an ERROR substring. Messages that satisfy these two checks are unsent messages. Note : Optimistically created messages do not have the ERROR substring in their IDs, so they are unaffected by this change. This ERROR substring gets introduced only after a failed mutation. Let's define the handleOnRetry function. It accepts the message as an argument. When executed, the function pops open a dialog that asks the user for confirmation to resend the message. Once the user confirms, the Apollo Client performs the AddMessage mutation, but this time, with the variables isRetry and createdAt . These two optional variables tell the AddMessage mutation's resolver to set the created message's timestamp to the timestamp provided by the createdAt variable, not the server's current timestamp. This ensures the messages are in the correct order the next time the application fetches the list of messages from the server. Visit the Codesandbox for the server here  for the implementation of the AddMessage mutation's resolver. If the mutation successfully completes, then the update callback function gets called with the Apollo Client cache and the result of the mutation. We extract out the message returned for the AddMessage mutation and update the currently cached message with the ID The updateFragment method fetches a Message object with an ID of Message:ERROR/<UUID> and replaces the fetched Message object's ID with the returned message's ID. With this update, the cached message will no longer be recognized as an unsent message. The fragment determines the shape of the fetched data. Within the src/types/fragments.ts file, define and export the CACHE_NEW_MESSAGE_FRAGMENT fragment. ( src/types/fragments.ts ) If the mutation ends up being unsuccessful, then we must prevent the onError link from duplicating the cached unsent message. If we passed an isRetry variable to the AddMessage mutation, then we should skip the duplication. Since we added two optional variables that can be passed to the AddMessage mutation, isRetry and createdAt , let's make several adjustments to account for these variables. Within the src/types/index.ts file, add the optional properties isRetry and createdAt to the AddMessageMutationVariables interface: Within the src/graphql/fragments.ts file, add the isRetry and createdAt variables to the AddMessage mutation string: If you look at the CodeSandbox for the server, then you will notice that... After making these changes, here's how the resend functionality should look: Lastly, let's render a "Delivered" status text beneath the current user's last sent message once the message's corresponding AddMessage mutation successfully completes. Within the <MessagesClient /> component, define the state variable lastDeliveredMessageId and update function setLastDelieveredMessageId with the useState Hook. lastDeliveredMessageId stores the ID of the last message sent by the current user. In the useMutation call, include within the passed options an onCompleted callback. This callback gets called as soon as the mutation's result data is available. Inside this callback, call setLastDeliveredMessageId with the sent message's ID. Within the JSX of the <MessagesClient /> component, place the "Delivered" status text right next to the <div className="mt-0.5" /> element. If the message could not be sent, then display a "Not Delivered" status text. Note : The i index comes from the map method's arguments. Reset the ID anytime the user attempts to send a message. So altogether, here's how everything should look: ( src/components/MessagesClient.tsx ) ( src/graphql/fragments.ts ) ( src/types/index.ts ) ( src/index.tsx ) If you find yourself stuck at any point while working through this tutorial, then feel free to visit the main branch of this GitHub repository here for the code. Try implementing optimistic UI patterns into your applications and reap the benefits of a faster, more fluid user experience. If you want to learn more advanced techniques with Apollo Client, GraphQL, React and TypeScript, then check out Fullstack React with TypeScript and Fullstack Comments with Hasura and React :

Thumbnail Image of Tutorial Optimistic UIs with React, Apollo Client and TypeScript (Part III) - Handling Errors

Optimistic UIs with React, Apollo Client and TypeScript (Part II) - Optimistic Mutation Results

Disclaimer - Please read the first part of this blog post here before proceeding. It walks through the initial steps of building a messages client that fetches messages from a GraphQL API server. If you are already familiar with the basics of Apollo Client, and only want to know how to update a UI optimistically (for mutation results), then download the project from the part-1 branch of the GitHub repository here and follow the directions in the README.md file to set up the project. In the second part of this tutorial series, we will implement the remaining functionality of the messages client: By the end of this tutorial, you will have recreated the optimistic UI found in Messages : For a user to send a message, the message client must send a request to the GraphQL API server to perform an addMessage mutation. Using the text sent by the user, this mutation creates a new message and adds it to the list of messages managed by the server. The addMessage mutation, which is defined in the GraphQL schema below, expects values for the text and userId variables. The text variable holds the new message's text, and the userId variable holds the ID of the user who sent the message. Once it finishes executing the mutation's resolver, the server responds back with the sent message. Unlike Apollo Client's useQuery Hook, which tells a GraphQL API server to perform a query (fetch data), Apollo Client's useMutation Hook tells a GraphQL API server to perform a mutation (modify data). Like the useQuery Hook, the useMutation Hook accepts two arguments: And returns a tuple with a mutate function and a result object: In the above snippet, the mutate function is named addMessage . The mutate function lets you send mutation requests from anywhere within the component. The result object contains the same properties as the result object returned by the useQuery Hook, such as data , loading and error . For the <MessagesClient /> component, the application can ignore this result object from the tuple. Since the mutation should cause the UI to update optimistically, the application does not need to present a loading message to indicate a mutation request being sent and processed. Therefore, it does not need to know when the mutation is in-flight. As for the data returned as a result of a successful mutation and the errors that the mutation may produce, we will handle those later on in this tutorial. The <MessagesClient /> component calls the useMutation Hook and destructures out the mutate function (naming it addMessage ) from the returned tuple. The <MessagesClient /> component contains an input field for the current user to type and send messages. First, let's create a ref and attach it to the <input /> element. The ref gives you access to the text typed into the <input /> element. Then, let's attach an event handler (named handleOnSubmit ) to the input field's parent <form /> element's onSubmit attribute that executes the addMessage mutate function when the user sends a message (submits the form with a non-empty input field). The handler calls addMessage , passing in (as the argument) an options object with a variables option, which specifies the values of all the variables required by the mutation. The addMessage mutation requires two variables: Once you finish making these adjustments to the <MessagesClient /> component, run/reload the application. When you send a message, the UI does not update with the message you just sent despite the server successfully processing the mutation request and sending back a response with this message. Typing message into input field: Sending message (submitting the form): Checking the response from the server: To update the UI with this message, we must update the messages stored in the Apollo Client cache. By default, Apollo Client stores the results of GraphQL queries in this local, normalized cache and uses cache APIs, such as cache.writeQuery and cache.modify , to update cached state. Anytime a field within the cache gets modified, queries with this field automatically refresh, which causes the components using these queries to re-render. In this case, the query within the <MessagesClient /> component has a messages field. Once the query resolves and returns with data that has this field (set to a list of messages), like this: Apollo Client normalizes and stores the data in the cache as a flat lookup table by...           As a result, the root level of the cache serves as a flat lookup table. To see this in action, add the update caching option to the addMessages options argument, and set this option to a function with two parameters: Apollo Client executes the update function after the addMessage mutation completes. When you log cache , you will see that cache contains metadata and methods available to an instance of InMemoryCache , which was specified as the Apollo Client's cache. Upon further inspection, you will find the fields messages and users (from the query operations) under cache.data.data.ROOT_QUERY . Logging  cache : Inspecting ROOT_QUERY  in the cache: Notice how Apollo Client stores the query responses using a normalization approach. The cache only keeps one copy of each piece of data, adjacent to ROOT_QUERY . This reduces the amount of data redundancy in the cache. Instead of repeating the same user data across every message, each message in the cache references the user by a unique identifier. This lets the Apollo Client to easily locate the user's data in its cache's flat lookup table. When you log mutationResult , you will see the message you just sent. To render this message to the UI, we must add it to the list of messages already cached by the Apollo Client. Any changes made to the cached query results gets broadcasted across the application and re-renders the components with those active queries. Within the update function, check if the mutation has completed and returned the sent message, like so: Then, we will directly modify the value of the cache's messages field with the cache.modify method. This method takes a map of modifier functions based on the fields that should be changed. Each modifier function supplies its field's current cached value as a parameter, and the value returned by this function replaces the field's current value. In this case, the map will only contain a single modifier function for the messages field. Note : cache.modify overwrites fields' values. It does not merge incoming data with fields' values. If you log existingMessagesRefs , then you see that it points to the value of the messages field under ROOT_QUERY (a list of objects with references to the actual messages in the cache's flat lookup table). To add the message to the cache, the modifier function must... Note : The fragment option of the argument passed to the cache.writeFragment method determines the shape of the data to write to the cache. It should match the shape of the data specified by the query. Reload the application. When you send a message, the UI now updates with the message you just sent. However, this UI update happens after , not before , the server successfully processes the mutation request and sends back a response with this message. To better observe this, simulate slow network speeds in the developer console and send a message again. Let's make the UI feel more responsive by optimistically updating it when the user sends a message. Displaying the mutation result before the GraphQL API server sends back a response gives the illusion of a performant UI and keeps the user engaged in the UI without any delays. When the server eventually sends back a response, the result from the server replaces the optimistic result. If the server fails to persist the data, then Apollo Client rollbacks the optimistic UI updates. To optimistically update the UI to show the message immediately after the user sends a message, provide an optimisticResponse option to the addMessages options argument, and set this option to the message that should be added to the cache. The message must be shaped exactly like the message returned by the addMessage mutation, and it must also include id and __typename attributes so that the Apollo Client can generate the unique identifiers necessary for the cache to remain normalized. Any optimistic update for the addMessage mutation goes through an optimistic mutation lifecycle : Altogether... ( src/components/MessagesClient.tsx ) If you find yourself stuck at any point while working through this tutorial, then feel free to visit the part-2 branch of this GitHub repository here for the code. If you simulate offline behavior in the developer tools and try to send a message, then you will see that the UI optimistically updates with the message for a brief moment before removing it from the UI. In the Messages app, when a message fails to be delivered, the message remains in the UI, but a red exclamation point icon appears next to the message to give the user a chance to resend the message. Continue on to the final part of this tutorial here , to learn how to handle such situations in optimistic UIs.

Thumbnail Image of Tutorial Optimistic UIs with React, Apollo Client and TypeScript (Part II) - Optimistic Mutation Results

I got a job offer, thanks in a big part to your teaching. They sent a test as part of the interview process, and this was a huge help to implement my own Node server.

This has been a really good investment!

Advance your career with newline Pro.

Only $30 per month for unlimited access to over 60+ books, guides and courses!

Learn More

Optimistic UIs with React, Apollo Client and TypeScript (Part I) - Project Overview

Liking a tweet on Twitter. Marking an e-mail as read in your G-Mail inbox. These type of simple, low-stake actions seem to happen so quickly that you can perform one action after another without having to wait for the previous to finish resolving. As the defining trait of optimistic UIs , these actions give the feeling of a highly responsive and instant UI. Psychologically speaking, they trick the user into thinking that an action has completed even though the network request it sends to the server has not been fully processed. Take, for example, the like button of a tweet. You can scroll through an entire feed and like every single tweet with zero delays between successive tweets. To observe this, open up a Twitter feed and your browser's developer console. Within the developer console, switch to the network tab and select the "Slow 3G" option under the throttling dropdown to simulate slow 3G network speeds. Slowing down network speeds lets us see the UI updates happen before the server returns a response for the action. Then, filter for network requests sent to a GraphQL API endpoint containing the text "FavoriteTweet" (in the request URL), which tells the server to mark the tweet as liked by the current user. When you click on a tweet's like button, the heart icon disappears, the like count increments by one and the text color changes to pink despite the network request still pending. While the server handles this request, the updates to the UI give the illusion that the server already finished processing the request and returned a successful response. In the below GIF, you can watch how liking multiple tweets, one after the other, immediately increments the like count of each tweet by one on the UI even if the server is busy working on previous requests. The user gets to like as many tweets as they want without waiting on any responses from the server. Upon receiving a response back from the server, the heart icon of the like button fades back in with an animation. Here's what a normal implementation of the like button might look like: Here's what Twitter's implementation of the like button looks like: Note : Twitter's UI never disables the like button. In fact, you can click on the like button as many times as you like. The UI will be updated accordingly, and the network requests for every click get sent to the server. By building UIs in this manner, the application's performance depends less on factors like the server's status/availability and the user's network connectivity. Since humans, on average, have a reaction time of 200 to 300 milliseconds , being delayed for this amount of time (or more) between actions (due to server response times) can cause not only a frustrating user experience, but also, hurt the brand's image. Being known for having a slow, unreliable, unresponsive UI makes users less likely to enjoy and engage with the UI. As long as the user perceives actions as being instant and working seamlessly, they won't ever question the application's performance. The key to adopting optimistic UI patterns is understanding the meaning of the word "optimistic." Optimistic means being hopeful and confident that something good will occur in the future. In the context of optimistic UIs, we should be confident that for some user action, the server, in at least 99% of all cases, returns a successful response, and in less than 1% of all cases, the server returns an error. In most situations, low-stake actions tend to be ideal candidates when deciding where to apply optimistic UI patterns. To determine whether an action is a low-stake action, ask yourself these questions: If the answer to all these questions is yes, then the action is a low-stake action, and thus, can update the UI optimistically with more benefits to the user experience than drawbacks. In the case of Twitter's like button: On the other hand, you should not consider optimistic UI patterns for high-stake actions, especially those involving very important transactions. For example, could you imagine a bank site's UI showing you that your check was successfully deposited, and then discovering days later, when you have to pay a bill due the next day, that it was not deposited because of the server happened to be experiencing a brief outage during that time? Think about how angry you would be at the bank and how this might sour your perception of the bank. Integrating optimistic UI updates into an application comes with challenges like managing local state such that results of an action can be simulated and reverted. However, applications built with React and Apollo Client have the necessary tools, features and APIs for easily creating and maintaining optimistic UIs. Below, I'm going to show you how to recreate a well-known optimistic UI found in a popular iOS app, Messages , with React and Apollo Client. When a user sends a message, the message appears to have been sent successfully even if the server has not yet finished processing the request. Once the server returns a successful response, there are no changes made to the UI except for a "Delivered" status text being shown beneath the most recently sent message. To get started, scaffold a new React application with the Create React App and TypeScript boilerplate template. For this project, we will be building a "public chatroom" that lets you choose which user to send messages as: Upon picking a user, the application displays the messages from the perspective of the selected user, and you can send messages as this user. Note : This server does not support real-time communications since that's outside the scope of this tutorial. You can add functionality for real-time communications with GraphQL subscriptions. Next, clone (or fork) the following GraphQL API server running Apollo Server. https://codesandbox.io/embed/apollo-server-public-chat-room-for-optimistic-ui-example-srb5q?fontsize=14&hidenavigation=1&theme=dark This server defines a GraphQL schema for a basic chat application with two object types: User and Message . It comes with a query type (for fetching user/s and message/s) and a mutation type (for adding a new message to the existing list of messages). Initially, this server is seeded with two users and twenty messages. Each resolver populates a single field with this seeded data that is stored in memory. Within the newly created React application, let's install several dependencies: Since the application will be styled with Tailwind CSS , let's set up Tailwind CSS for this application. Within the tailwind.config.js file, add the paths glob pattern ./src/**/*.{js,jsx,ts,tsx} to tell Tailwind which type of files contain React components. Since the UI features an input field, we should also the @tailwindcss/forms plugin with the strategy option set to class to leverage Tailwind CSS form component styles via CSS classes. ( tailwind.config.js ) Delete the src/App.css file and remove all of the default CSS rules in the src/index.css file. Within this file, add the standard @tailwind directives: ( src/index.css ) Add several empty directories to the src directory: To initialize an ApolloClient instance, import the Apollo Client and pass it a configuration object with two options: To make the Apollo Client instance available throughout the entire React application, wrap the <App /> component within the provider component <ApolloProvider> , which uses React's Context API. Here's what the src/index.tsx file should look like: ( index.tsx ) The application contains two child components: Since both components must know who the current user is, and the <UsersList /> component sets the current user, let's define a React context AppContext to make the current user globally available to the application's component tree. Within the src/context directory, add an index.ts file: Then, define the React context AppContext . Its value should contain a reference to the current user ( currentUser ) and a method for setting the current user ( changeCurrentUser ). ( src/contexts/index.ts ) Although we initialize the value of AppContext to an empty object, we will later set this context's value in the <App /> component, where we will pass it its actual value via its provider component's value prop. The AppContextInterface interface enforces the types allowed for each method and value specified in the context's value. You may notice a User type that is imported from a src/types/index.ts file. Within the src/types directory, add an index.ts file: Based on the GraphQL schema, define a User interface. ( src/types/index.ts ) Within the src/App.tsx file, import AppContext and wrap the child components and elements of the <App /> component with the AppContext.Provider provider component. Inside of the <App /> component's body, we define a state variable currentUser , which references the currently selected user, and a method changeCurrentUser , which calls the setCurrentUser update function to set the current user. Both currentUser and changeCurrentUser get passed to the AppContext.Provider provider component's value prop. These values satisfy the AppContextInterface interface. ( src/App.tsx ) The <UsersList /> component fetches a list of users from the GraphQL API server, whereas the <MessagesClient /> component fetches a list of messages from the GraphQL API server. To fetch data from a GraphQL API server with Apollo Client, use the useQuery Hook. This Hook executes a GraphQL query operation. It accepts two arguments: And returns a result object, which contains many properties. These are the most commonly used properties: These properties represent the state of the query and change during its execution. They can be destructured from the result object and referenced within the function body of the component, like so: For more properties, visit the official Apollo documentation here . Once it successfully fetches data from the GraphQL API server, Apollo Client automatically caches this data locally within the cache specified during its initialization (i.e., an instance of InMemoryCache ). Using a cache expedites future executions of the same queries. If at a later time, Apollo Client executes the same query, then Apollo Client can get the data directly from the cache rather then having to send (and wait on) a network request. Within the src/components/UsersList.tsx file, define the <UsersList /> component, which... ( src/components/UsersList.tsx ) Once the data has been successfully fetched, the component renders a list of users who are members of the "public chatroom." When you click on one of the users, you select them as the current user. A check mark icon appears next to the user's name to indicate that it has been selected. Since the query returns a list of users, the UsersQueryData interface contains a users property that should be set to a list of User items, like so: ( src/types/index.ts ) Note : It should match what's specified by the GraphQL query string that's passed to the useQuery Hook. To refresh the cached data with the latest, up-to-date data from the GraphQL API server, you can: To know when Apollo Client is refetching (or polling) the data, destructure out the networkStatus value from the result object, and check if it equals NetworkStatus.refetch , which indicates an in-flight refetch, or if it equals Network.poll , which indicates an in-flight poll. Note : The notifyOnNetworkStatusChange networking option tells Apollo Client to re-render the component whenever the network status changes (e.g., when a query is in progress or encounters an error). For a full list of network statuses you can check for, click here . Like the <UsersList /> component, the <MessagesClient /> component also fetches data (in this case, a list of messages) by calling the useQuery Hook. When rendering the messages, the current user's messages are aligned to the right-side of the messages client. These messages have a blue background with white text. All other messages are aligned to the left-side of the messages client. By adding the sender's initials and name to each of these messages, we can tell who sent which message. ( src/components/MessagesClient.tsx ) All that's missing from the message client, UI-wise, is an input field for sending messages. Below the messages, add a form with an input field and send button. Altogether... ( src/components/MessagesClient.tsx ) If you find yourself stuck at any point while working through this tutorial, then feel free to visit the part-1 branch of this GitHub repository here for the code. Thus far, we learned how companies like Twitter adopt optimistic UI patterns to deliver faster, snappier user experiences. We set up the project with Apollo Client, Tailwind CSS and TypeScript, and we built a UI that queries data from a GraphQL API server. Continue on to the second part of this tutorial here , in which we implement the remaining functionality: Specifically, we will dive into the useMutation Hook and learn how to manipulate data within the Apollo Client cache to update the UI optimistically.

Thumbnail Image of Tutorial Optimistic UIs with React, Apollo Client and TypeScript (Part I) - Project Overview