FOUNDERS

Optimistic updates: How to make your app feel instant to the user

Saida Kurtanović
Oct 21st, 2026
9min read
JUMP TO SECTION
A place for quotes, summaries, key points or something else that you want to highlight in this blog post.

Each problem is unique in its own way. Sometimes you get stuck with some bug without having an idea what to do next except to use joker “call other dev”. Sometimes you miss an estimate for the new feature by a lot just because you haven’t planned it properly or you have missed some obvious points because you haven’t asked the right questions at the right time. 

As you gain experience, you probably start having your own way of approaching these problems, set of some unwritten. You formed these rules, by bleeding on some problems, basically from your mistakes and falls. Would it be better to gain advantage by developing/learning a strategy much earlier in a career, and avoid unnecessary bleeding.

Having an exact strategy on how to approach problems in development can make your life easier. Even though you don’t need to follow exact steps every time, you will by time incorporate these steps unconsciously in your routine. Start by forcing yourself to follow these steps, write them somewhere in a visible place in your (home)office. Let’s try to define strategy!

Handling features

Even though your first tasks on your first real jobs will be to fix some minor bugs, soon after you will start working on some startup or get an opportunity to work on an important company project, there you will need to implement some features.

We have divided feature implementation in following steps:
- Analysis
- Estimation
- Coding
- Testing

Analysis

Getting to know the feature you are developing is the most important thing, you need to know every corner of it and you need to know answers to all questions about it. Start by approaching the problem from a critical standpoint and label all those uncertainties you have and clear those things. Forget about all the technical details and just think of a functionality from a user perspective as if you are going to use it. Communicating properly with others in a team will be the only right way that will give results. So, communicating with a designer, PM , other devs, product and anyone else involved will save you a lot more time and meetings in a later phase.

This is the phase where most engineers spend less time than needed mostly because of:
- firm deadlines and they try to save time in analysis and estimation
- not willing to participate and communicate in meetings which are related to analysis
- because of mindset that these meetings are waste of time
- If you are less experienced engineer you might be having a fear of asking stuping questions
- jump on coding directly and try to figure out things during development


As a part of analysis you need to do following:
‍
1. Read your ticket
Read your ticket carefully. Make sure all acceptance criterias are clear to you. If there are no AC’s, there must be some product or project manager who should explain the ticket to you. 

After you have read your ticket, if you are working on client-side there should be some design that will show you what you need to code. 

2. Design analysis
You need to analyze design, and communicate any potential discrepancies or unclarities with the designer.
- Check if designer is following design system and using standardized components
- Check if implementation complexity is on a much higher level then benefits we are getting?

3. Compare design and acceptance criteria (AC)
Compare ticket description (and AC’s) with implemented design. If they are not in sync, communicate those differences with products, PM and designer. You need to have a clear picture, and know everything about the feature you are going to implement.

4. Ask questions 
- Don’t be afraid to ask questions about anything.
- Asking questions and proper communication is in general one of the most important things that will take you to successfully finalize your tasks on time.

5.  Check the codebase and analyze technical implementation
After you are clear about what you need to implement, you should take a quick look at the current state of the app and check the codebase where you are adding this feature. Think from a logical standpoint, are there some things that are not possible to implement in a way that non-tech people have envisioned it.  See if you have proper knowledge of the technology you are working on, try figuring out if you can implement requested or you need guidance from more senior dev.
Ask for opinions from people who have already touched that part of the app.

6. Client side - server side sync
Also, if you are interacting with the backend side (or vice-versa), you need to take into account if you have everything ready from the other side. Also, point out any additional work that should be done on the other side before you can complete your task.

7.  Divide ticket into subtasks
After you have verified all the stuff mentioned, you need to create your own plan of the implementation. Divide ticket into minor subtasks, either creating those subtasks in reality or having it just on a piece of paper. 

8. Create implementation plan
Write down your plan and keep notes of any future adjustments.

Estimation

When approaching ticket estimation, the best case scenario is that you have done all the steps from analysis and you come prepared for the pointing session. Don’t be afraid to postpone estimations, or to reject estimating when things are not clear enough to you. Once you estimate the ticket, any broken deadline from the dev side is on you. Of course, some unexpected things will always happen, issues will occur, but you need to make sure that you did your best.

Why are most estimates inaccurate ?
- Not investing enough time in analysis phase - being unprepared
- Being too optimistic and not anticipating unexpected events
- Blind guessing

When estimating, you should keep in mind following things:
‍
1. Estimate each subtask separately
Estimate each subtask separately and in the end just summarize the points. 

2. Consider PR reviews 
You should consider also adding some points for PR reviews - because those can take some time, especially when complex logic is implemented.

3. Meetings and communication in the team
Consider time you will need to invest into communication with the team if needed (ex integration)

4. Dev Testing 
Dev testing is a really important part to minimize bugs produced, so that will take some time also. Also, if you are working in different environments, you need to test on all those also. If deploying is your responsibility, you should take into account that time also.

5. Unexpected things
And just when you thought you had your final estimate, pause for a second, and add some points for unexpected things. Always expect the unexpected.

Coding

Once the time has come for you to start coding your feature, just go over your plan and go task by task. Seek help on time once you are stuck, at least talk with someone or use the rubber duck method.

1. Split your code
It would be best to have one feature branch on which you will merge multiple minor subtasks - meaning split your code to get approvals faster. Dev test everything in detail.

2. Escalate the issues - push deadlines on time
If you encounter any issue, either something is much harder to implement or you notice something that you have missed, don’t be afraid to escalate the issue, to contact anyone involved in it. Be honest, don’t be optimistic in a way that the newly found issue won’t affect deadlines, sooner you accept it and push the deadline for a valid reason you will lower stress and pressure for everyone, most importantly for you.

3. Compare implementation with AC’s, design and description
When you finish coding and you are just about to create a PR, go over AC’s, ticket description and design and verify that your work is in sync with the requirements.

4. Self-review your code and seek improvements
Before creating a PR, you also need to do self-review of your code, and seek improvements. See if you can refactor something or do some other code improvements.

5. Think of edge cases and dev test everything once more
Dev test everything in detail - this will keep bugs away from you.
Try to find possible edge cases, be as destructive to your code as you can.

6. Create a PR 
When creating a PR, add comments with explanations for problematic places where you feel that someone could add a comment. It is always better to comment first, to show that you are aware the code is not easy to understand.

7. Resolve comments, finalize discussions
To merge your code it is really important to resolve/reply to comments on your PR as soon as possible, so you don’t get conflicts and also to speed up merging PR. Don’t leave your PR in a vacuum for some time.

Testing

In the end, when your code is merged, test it on the environment once more to make sure that it really works.

Mistake that a lot of developers do is they skip environment specific testing and directly move ticket to QA and issues might arise:
- Code was not deployed because of build issues and QA returns ticket
- There are some environment specific bugs which you did not covered during dev testing
- Someone else changes uncovered one of the cases you haven’t handled properly

Handling bugs
‍
While most of these rules and principles apply to bugs, there are some key differences in the approach that we will mention in this section.

Before we delve into the details on how to tackle bugs that have appeared in the feature development process, we should quickly mention the types of bugs that you might encounter.
‍
‍
Types of bugs

•
Styling
◦ Description: Styling bugs refer to issues related to how elements are presented visually on the user interface.
◦ Examples: Incorrect colors, font sizes, margins, paddings, or issues with layout responsiveness.
‍
• Logical mistakes
◦ Description: Logical mistakes are errors in the application's logic flow, where the program does not perform the intended actions due to flawed reasoning or oversight.
◦ Examples: Infinite loops, unreachable code, or unintended consequences of conditional statements
• Business logic
‍
◦ Description: Business logic bugs are errors related to how the application handles specific rules and processes that are essential to the core functionality of the software.
◦ Examples: Incorrect calculations, flawed decision-making processes, or rules not applied correctly.

• Rendering
◦ Description: Rendering bugs occur when the content or elements on a webpage or application do not display correctly or as intended.
◦ Examples: Misaligned elements, overlapping content, or elements not appearing in the correct order.

• Cross browser compatibility
◦ Description: Cross browser compatibility bugs occur when a website or application behaves differently across various web browsers or versions.
‍◦ Examples: Features not working in certain browsers, layout issues specific to a browser's rendering engine, or performance variations.

• Data issues
◦ Description: Data issues involve problems with the integrity, accuracy, or consistency of data within the application.
‍◦ Examples: Incorrect data retrieval, data corruption, or mismatches between different data sources.

• State management
◦ Description: State management bugs involve problems with how data is stored and managed within an application, especially across different parts of the application.
‍◦ Examples: Inconsistent data updates, incorrect data being displayed, or data loss under certain conditions

These aforementioned bug types are most common bug types that you will encounter, some other notable mentions are:

• Compatibility issues
◦ Description: Compatibility bugs arise when the software does not work correctly with other software components or systems it needs to interact with.
‍◦ Examples: API changes not accounted for, incompatible library versions.

• Performance bugs
◦ Description: Performance bugs impact the speed or responsiveness of the software.
‍◦ Examples: Slow loading times, high memory usage, or inefficient algorithms causing delays.

• Security bugs
◦ Description: Security bugs involve vulnerabilities that could be exploited by malicious users or code.
‍◦ Examples: Incorrect authentication setup, missing authentication middleware.

• Localization issues
◦ Description: Localization bugs occur when the software does not behave correctly or display content properly in different languages or locales.
‍◦ Examples: Text truncation, incorrect date/time formatting, or issues with character encoding.

• Deployment bugs
◦ Description: Installation or deployment bugs prevent the software from being installed or deployed correctly.
‍◦ Examples: Configuration errors, incorrect environment variables

While all of these bug types could be a separate topic itself, we were thinking of a general way to handle all of these variations. It is not mandatory to take all these steps when fixing your bug, but it is a good idea to take all them into consideration when doing your development to minimize unexpected issues that might occur from your fix.

We have split this method into 4 parts: ticket analysis, problem analysis, coding, testing

Ticket analysis
‍
1. Read the ticket carefully
a. Make sure you fully understand the problem at hand
b. Read the documentation related to the bug at hand to make sure you didn’t miss any hidden requirements or acceptance criteria
c. Ask questions to people who are reporters of the issue as well as your PM or team lead
d. Make sure everything is clarified
2. Reproduce the bug
a. Sometimes a bug that is reported is not even a bug, a lot of the times the reporter of the issue did not go through previous point before the issue is raised
b. Make sure you understand under what circumstances this error is reproduced
c. If there are some more issues related than what is being reported, notify the PM/QA about it
3. Analyze
a. Confirm whether the issues is caused by client side or server side changes
b. Confirm whether the issues might be caused by another team
c. With proper analysis, there might be no need for development, problems are often solved by communication with the team
d. Make sure you document your analysis (post your findings in the comments of the ticket or similar)
e. Assume the feature is a black box and write all the IN-OUT cases for the feature and highlight those for which the problem presents itself

Problem analysis‍

1. Search where the issue happens on a higher level
a. Try to locate the screen/component where you think the problem resides
b. What is happening and how should it behave differently ?
c. When did the issue start to appear ? Get some insights from the Git Lens extension.
2. Debug the code to find the exact location of the culprit
a. In some cases, try not to look at the part where you think the bug is happening, sometimes you have to go a few steps back or a few steps further to locate the actual cause of the issue.
b. Read the lines of code line by line, understand what is happening under the hood
3. Think of the IN-OUT parameters that you wrote in the previous section, check if the line that you suspect produces unexpected results for the highlighted cases

Coding

1. By the time you come down to this part, you most likely know what change to make to fix the bug at hand
2. Visualize the problem, go over the exact steps that you have tried and didn’t work (Rubber duck)
3. If you don’t know how to do it (even after the analysis step), ask for help from a more senior developer
4. If you have a sense that you could write this code cleaner/better write it down 
a. Always do some minor refactoring if you have the time to do so.

Testing

1. Developer testing
a. By doing proper developer testing, you will save valuable development/testing time further and lower the chances of encountering a “boomerang” effect with your bug ticket. Just think of the path the ticket has to go before it comes back to you.
b. Use the IN-OUT cases that you wrote in one of the previous steps to check if all works as expected
c. This includes testing your fix after your changes were deployed to the staging environment
‍
One thing to note from all of this is that even the most senior don’t go through all of these exact steps, the point of this guide was that all of these points make a strategic way to tackle issues and to make sure they have tried with everything that is in their power. Usually, when this method doesn’t help, the developer mostly relies on the help of someone who had previous experience with a similar issue, someone more experienced or perhaps trying to fish for an answer on Stackoverflow which should always be the last resort.

Each problem is unique in its own way. Sometimes you get stuck with some bug without having an idea what to do next except to use joker “call other dev”. Sometimes you miss an estimate for the new feature by a lot just because you haven’t planned it properly or you have missed some obvious points because you haven’t asked the right questions at the right time. 

As you gain experience, you probably start having your own way of approaching these problems, set of some unwritten. You formed these rules, by bleeding on some problems, basically from your mistakes and falls. Would it be better to gain advantage by developing/learning a strategy much earlier in a career, and avoid unnecessary bleeding.

Having an exact strategy on how to approach problems in development can make your life easier. Even though you don’t need to follow exact steps every time, you will by time incorporate these steps unconsciously in your routine. Start by forcing yourself to follow these steps, write them somewhere in a visible place in your (home)office. Let’s try to define strategy!

Handling features

Even though your first tasks on your first real jobs will be to fix some minor bugs, soon after you will start working on some startup or get an opportunity to work on an important company project, there you will need to implement some features.

We have divided feature implementation in following steps:

  1. Analysis
  2. Estimation
  3. Coding

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6
Block quote

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript

Each problem is unique in its own way. Sometimes you get stuck with some bug without having an idea what to do next except to use joker “call other dev”. Sometimes you miss an estimate for the new feature by a lot just because you haven’t planned it properly or you have missed some obvious points because you haven’t asked the right questions at the right time. 

As you gain experience, you probably start having your own way of approaching these problems, set of some unwritten. You formed these rules, by bleeding on some problems, basically from your mistakes and falls. Would it be better to gain advantage by developing/learning a strategy much earlier in a career, and avoid unnecessary bleeding.

Having an exact strategy on how to approach problems in development can make your life easier. Even though you don’t need to follow exact steps every time, you will by time incorporate these steps unconsciously in your routine. Start by forcing yourself to follow these steps, write them somewhere in a visible place in your (home)office. Let’s try to define strategy!

Handling features

Even though your first tasks on your first real jobs will be to fix some minor bugs, soon after you will start working on some startup or get an opportunity to work on an important company project, there you will need to implement some features.

We have divided feature implementation in following steps:

  1. Analysis
  2. Estimation
  3. Coding

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6
Block quote

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript

Each problem is unique in its own way. Sometimes you get stuck with some bug without having an idea what to do next except to use joker “call other dev”. Sometimes you miss an estimate for the new feature by a lot just because you haven’t planned it properly or you have missed some obvious points because you haven’t asked the right questions at the right time. 

As you gain experience, you probably start having your own way of approaching these problems, set of some unwritten. You formed these rules, by bleeding on some problems, basically from your mistakes and falls. Would it be better to gain advantage by developing/learning a strategy much earlier in a career, and avoid unnecessary bleeding.

Having an exact strategy on how to approach problems in development can make your life easier. Even though you don’t need to follow exact steps every time, you will by time incorporate these steps unconsciously in your routine. Start by forcing yourself to follow these steps, write them somewhere in a visible place in your (home)office. Let’s try to define strategy!

Handling features

Even though your first tasks on your first real jobs will be to fix some minor bugs, soon after you will start working on some startup or get an opportunity to work on an important company project, there you will need to implement some features.

We have divided feature implementation in following steps:

  1. Analysis
  2. Estimation
  3. Coding

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6
Block quote

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript

Each problem is unique in its own way. Sometimes you get stuck with some bug without having an idea what to do next except to use joker “call other dev”. Sometimes you miss an estimate for the new feature by a lot just because you haven’t planned it properly or you have missed some obvious points because you haven’t asked the right questions at the right time. 

As you gain experience, you probably start having your own way of approaching these problems, set of some unwritten. You formed these rules, by bleeding on some problems, basically from your mistakes and falls. Would it be better to gain advantage by developing/learning a strategy much earlier in a career, and avoid unnecessary bleeding.

Having an exact strategy on how to approach problems in development can make your life easier. Even though you don’t need to follow exact steps every time, you will by time incorporate these steps unconsciously in your routine. Start by forcing yourself to follow these steps, write them somewhere in a visible place in your (home)office. Let’s try to define strategy!

Handling features

Even though your first tasks on your first real jobs will be to fix some minor bugs, soon after you will start working on some startup or get an opportunity to work on an important company project, there you will need to implement some features.

We have divided feature implementation in following steps:

  1. Analysis
  2. Estimation
  3. Coding

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6
Block quote

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

Block quote

Ordered list

  1. Item 1
  2. Item 2
  3. Item 3

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript

Optimistic updates: how to make your app feel instant to the user

Anyone who has worked on a larger application has run into the same situation: the user clicks a button, the mutation goes off to the backend, and then they wait. Half a second, a second, sometimes more, because there is a lot of data, the queries are complex, or the network is simply bad. For us as developers it is "just a request". For the user, it is the moment they wonder whether the click was registered at all.
What we usually do is the classic flow: send the mutation, wait for the response, refetch the data, show the new state. It works, but it is slow and expensive. A refetch after every mutation means one more round-trip to the server, one more spinner, and one more moment in which the UI "freezes".
Optimistic updates are a strategy that solves exactly this problem. The idea is simple: assume the mutation will succeed, show the result in the UI immediately, and let the backend finish the job in the background. If something goes wrong, revert to the previous state. Let's walk through it step by step.

Two ideas from UI research and distributed systems explain why this works. The first is perceived latency: users experience a response under roughly 100 ms as instant, and anything up to about a second as "the system is working". Beyond that, attention drifts and trust drops. Most mutations land in exactly that second range, so the goal is not to make the network faster but to move the visible feedback under the 100 ms threshold.

The second idea is that the client already holds a copy of the server state. Every list you render is a cached projection of what the backend returned at some moment. A mutation is just a known transformation of that projection, and applying it locally is the same operation the server will perform, only earlier. The server stays the source of truth; the client is temporarily one step ahead of it. In distributed-systems terms this is eventual consistency: the two copies may briefly disagree, but they converge once the response arrives or the rollback runs.

What are optimistic updates?

The word "optimistic" here means exactly what it says: we are optimistic about the outcome of the mutation. If we have a system that works well, the vast majority of mutations go through. So why should the user wait for confirmation every single time for something that is almost certain to happen?

The classic (pessimistic) approach:
  • 1. The user clicks
  • 2. We send the request
  • 3. We wait for the response
  • 4. We refetch the data
  • 5. We update the UI
The optimistic approach:
  • 1. The user clicks
  • 2. We update the UI immediately
  • 3. We send the request
  • 4. If it succeeds, we do nothing (the UI is already correct)
  • 5. If it fails, we revert the UI to the previous state

The difference is in who waits for whom. In the first case, the user waits for the server. In the second case, the server "catches up" with the UI.

Case 1 (slow): UI waits for server confirmation
User action
↓
Server request
↓
Server response (success or fail)
↓
Update UI
Case 2 (fast): Optimistic UI update flow
User action
↓
Instant UI update
↓
Server request in background
↓
Server response
↓
Success: UI is already updated
Failure: revert UI (rollback)

Local state instead of refetching

The first thing you need to accept is that there is no need to refetch data after a mutation. It is enough to have a state that contains all the values from the query response, and which you update manually when a mutation is executed.

The rules are simple and follow the HTTP methods:

  • ● POST (create): the state is extended with one more element
  • ● PUT / PATCH (update): the element in the state is replaced with the new value
  • ● DELETE (delete): the element is removed from the state

Let's take a list of comments as an example:

const [comments, setComments] = useState(initialComments);
POST method:
const addComment = async (text) => {
  const tempId = crypto.randomUUID();
  const newComment = { id: tempId, text, pending: true };
  setComments((prev) => [...prev, newComment]);
  const saved = await api.createComment({ text });
  // replace the temporary element with the one returned by the server
  setComments((prev) =>
    prev.map((c) => (c.id === tempId ? saved : c))
  );
};
PUT method:
const editComment = async (id, text) => {
  setComments((prev) =>
    prev.map((c) => (c.id === id ? { ...c, text } : c))
  );
  await api.updateComment(id, { text });
};
DELETE method:
const deleteComment = async (id) => {
  setComments((prev) => prev.filter((c) => c.id !== id));
  await api.deleteComment(id);
};

A few important observations:

  • 1. The UI is updated before the await. That is the whole point. The user sees the result immediately.
  • 2. For POST we use a temporary ID. The server will generate the real ID, and in the meantime we need something to find the element by and replace it once the response arrives.
  • 3. No refetch. The server is the source of truth, but we already know what the truth is, because we sent it ourselves.

Pagination issue

The rules above assume the state is a single flat list. In practice the list is often paginated, and that is where the theory gets messier. With offset pagination the client holds pages 1 to N; a POST that adds an element on the client shifts every page boundary by one, so the next fetch of page N+1 will either repeat the last element or skip one. With cursor pagination the boundaries are stable, but a new element still has to be placed in the right sort position, and the "total count" the server reported is now off by one. Infinite-scroll lists are the same problem in a different coat.

There is no universal fix, only a decision you have to make consciously: insert the new element at the top or bottom of the visible list and accept a temporary sort inconsistency, or update only the page you know it belongs to. Libraries like React Query model this explicitly (a paginated query is an array of pages, and the optimistic updater maps over them), which is a good hint that this is a first-class problem, not an edge case.

What if the mutation fails?

This is the part most developers skip the first time they implement optimistic updates, and this is where the worst bugs come from: the user sees a comment that was never actually saved, or thinks they deleted something, only for it to come back on the next refresh.

The solution is snapshot and rollback. Before you change the state, remember its current value. If the mutation ends up in the catch block, revert the state to that snapshot.
const deleteComment = async (id) => {
  const snapshot = comments; // remember the state before the mutation
  setComments((prev) => prev.filter((c) => c.id !== id));
  try {
    await api.deleteComment(id);
  } catch (error) {
    setComments(snapshot); // rollback
    toast.error("Delete failed. Please try again.");
  }
};
Rules to follow when rolling back:
  • 1. Always take a snapshot before the change.
  • 2. Notify the user. A rollback without a message is worse than a slow UI. The user saw something get deleted, and then it suddenly came back. Tell them what happened.
  • 3. Think about concurrent mutations. If the user clicks three times within a second and the third one fails, you must not revert the state to what it was before the first one. For simple cases a snapshot is enough; for more complex ones, consider a mutation queue or a library that handles this for you (React Query, SWR, Apollo).

If you have a system that works well, rollbacks rarely happen. But "rarely" is not "never", and it is precisely that rare case that makes the difference between an app the user trusts and one they don't.

useOptimistic: built-in support in React 19

React 19 ships a hook built for exactly this problem: useOptimistic. Its purpose is to have the UI show the result of an action immediately, before the backend finishes the job.

The classic example is a "Like" button. You click, the request takes 500 ms, and instead of waiting for the heart to change, you show "Liked" right away.

function LikeButton({ isLiked, likePost }) {
  const [optimisticLiked, setOptimisticLiked] = useOptimistic(isLiked);
  const handleClick = () => {
    startTransition(async () => {
      setOptimisticLiked(true);
      await likePost();
    });
  };
  return (
    <button onClick={handleClick}>
      {optimisticLiked ? "Liked" : "Like"}
    </button>
  );
}

The user sees "Liked" immediately, even though the backend hasn't responded yet.

The most important thing to understand: useOptimistic does not change the real (canonical) state. It creates a temporary view of the state while the action is in progress. When the action finishes, React automatically discards the optimistic view and falls back to the real isLiked coming from props or the query. This means two things:
  • ● You get a rollback for free. If likePost() throws, the action ends, the optimistic view disappears, and the UI returns to the real value. You don't have to remember a snapshot manually.
  • ● You must update the real state after success. If isLiked doesn't change after a successful mutation (e.g. through revalidation or a setState in the parent), the UI will "jump" back to the old value.

The hook also accepts a second argument, a reducer function, for when you need something more complex than replacing a value, e.g. adding an element to a list:

const [optimisticComments, addOptimisticComment] = useOptimistic(
  comments,
  (current, newComment) => [
    ...current,
    { ...newComment, pending: true },
  ]
);

You can find more about the hook in the official React documentation.

When (not) to use optimistic updates

Like any strategy, this one isn't for everything. Before you introduce it, ask yourself a few questions, just as you would in the analysis phase of any other feature.

Good candidates:
  • ● actions that almost always succeed (like, toggle, reorder, adding to a list);
  • ● actions whose result you can predict on the client;
  • ● actions the user performs often, where every spinner gets in the way.
Bad candidates:
  • ● payments and financial transactions;
  • ● actions whose result depends on the server (e.g. the server calculates a price, generates a document, checks availability);
  • ● actions where a wrong display is worse than waiting (deleting an account, sending an email);
  • ● actions whose position in the list depends on server-side sorting, filtering or pagination you cannot reproduce on the client.
The golden rule: if the user can make a decision based on the optimistic view that they cannot undo later, don't be optimistic.



One thing to note from all of this is that even the most senior don’t go through all of these exact steps, the point of this guide was that all of these points make a strategic way to tackle issues and to make sure they have tried with everything that is in their power. Usually, when this method doesn’t help, the developer mostly relies on the help of someone who had previous experience with a similar issue, someone more experienced or perhaps trying to fish for an answer on Stackoverflow which should always be the last resort.

Conclusion

Optimistic updates are not a trick, they are a shift in mindset. Instead of the user waiting for the server, the server catches up with the UI. The result is an application that feels fast even when the network isn't.

If you take three things away from this text, let them be these:

  • 1. No refetch after a mutation. You already know what changed, so update the state manually according to the mutation type (POST adds, PUT changes, DELETE removes).
  • 2. Always have a rollback. A snapshot before the change, a revert in the catch block, and a clear message to the user.
  • 3. Use useOptimistic where it makes sense. React 19 gives you a temporary view of the state and automatic rollback, but you still have to update the real state yourself.

As with any development strategy, at first you will have to force yourself to think about snapshots, rollbacks, and edge cases. Over time it will become routine, and your users will notice the difference before you even realize it.

READ MORE

Similar articles

FOUNDERS
Problem Solving - Features
Ahmed Selimović
August 28th, 2024