danilodev
Published on

Level Up Your Frontend: UX Tactics for Developers

Authors

Level Up Your Frontend: UX Tactics for Developers

User experience (UX) isn’t only for designers. As frontend developers, we have a big role in how users interact with our apps. If the user interface (UI) isn’t done well, it can lead to frustration, cause users to leave, and give the product a bad reputation. In this post, we’ll look at some simple ways to make web experiences better for users.

Making Your Web App Accessible to Everyone

Accessibility (a11y) is not optional; it's crucial. Making your site accessible means that everyone, including users with disabilities, can use it effectively.

Use semantic HTML elements correctly (<button>, <nav>, etc.), and leverage ARIA attributes to enhance accessibility (e.g., aria-label, aria-describedby). Make sure keyboard navigation is logical and intuitive, using the Tab key to move between elements, and also make sure elements have proper focus indicators.

Choose colors that have sufficient contrast, and test your application with screen readers to find issues. Libraries like React Aria and Radix UI can be incredibly helpful for building accessible components.

Handling Loading States and Errors Effectively

An empty page while data loads isn't a great user experience. Use loading skeletons – placeholders that mimic the structure of the content that's going to load. This lets users know something is happening and gives them a sense of what to expect.

Always provide clear, user-friendly error messages that explain the issue. After an error, provide a clear ‘Retry’ button that lets users attempt the action again without reloading the entire page, and make sure if part of your app fails, it doesn't crash the whole page. Also, make sure to log errors for easier debugging and fixing.

Using Optimistic UI for Faster Feedback

Optimistic UI is a technique that makes your application feel lightning-fast. When a user takes an action (e.g., liking a post), update the UI immediately, as if the server request has already succeeded. This gives the user immediate feedback, even if the server hasn't responded.

function handleLike(postId) {
  // Optimistically update the UI
  setPosts((prevPosts) =>
    prevPosts.map((post) => (post.id === postId ? { ...post, likes: post.likes + 1 } : post))
  )

  // Send the request to the server
  fetch(`/api/posts/${postId}/like`, { method: 'POST' })
    .then((response) => {
      if (!response.ok) {
        // Handle errors by reverting UI change
        setPosts((prevPosts) =>
          prevPosts.map((post) => (post.id === postId ? { ...post, likes: post.likes - 1 } : post))
        )
        throw new Error('Failed to like post')
      }
    })
    .catch((error) => {
      console.error(error)
      // Show a user-friendly error message (e.g., a toast)
    })
}

Of course, you need to implement proper error handling. If the server request fails, revert the optimistic update and show a meaningful error message.

Optimistic updates work great for actions where instant feedback is important (likes, comments, etc.). But you should avoid using it for very important data integrity situations.

Improving Performance with Caching

Why fetch the same data over and over? Caching data can significantly improve your application's speed and reduce network load. You can use browser caching, local storage, or a state management solution.

Use strategies like stale-while-revalidate to serve cached data instantly while fetching fresh data in the background. This ensures the user always sees fresh information without having to wait. Consider using libraries like react-query or swr which help you manage caching and background updates.

import { useQuery } from 'react-query'

function MyDataComponent() {
  const { data, isLoading, error } = useQuery('my-data', () =>
    fetch('/api/data').then((res) => res.json())
  )

  if (isLoading) return <p>Loading data...</p>
  if (error) return <p>Error loading data: {error.message}</p>
  return (
    <div>
      {/* Render data here */}
      {JSON.stringify(data)}
    </div>
  )
}

Be careful about using localStorage for sensitive data, if there is any.

Fixing the Spinner Dance (Improving Loading Experiences)

Have you ever been annoyed by a spinner that flashes on the screen for a split second? Or even worse, been waiting for what feels like an eternity on a page that doesn't display anything while loading? Here are some tips on dealing with those common annoyances.

The biggest problem here is when a network request is too fast that displaying a spinner is more harmful than helpful. Why bother showing a loading spinner when the data loads in less than 50ms?

A straightforward fix is to add a delay. Show the spinner only if the request takes longer than, say, 200ms.

import { useState, useEffect } from 'react'

function useDelaySpinner(isLoading, delay = 200) {
  const [showSpinner, setShowSpinner] = useState(false)

  useEffect(() => {
    if (isLoading) {
      const timer = setTimeout(() => {
        setShowSpinner(true)
      }, delay)
      return () => clearTimeout(timer)
    } else {
      setShowSpinner(false)
    }
  }, [isLoading, delay])

  return showSpinner
}

function MyComponent({ fetchData }) {
  const [isLoading, setIsLoading] = useState(false)
  const showSpinner = useDelaySpinner(isLoading)

  useEffect(() => {
    setIsLoading(true)
    fetchData().then(() => setIsLoading(false))
  }, [])

  return <>{showSpinner && <p>Loading...</p>}</>
}

However, what happens when a request takes 210ms? We'd see a spinner for just 10ms, which can be a really annoying flicker. To avoid this, consider a minimum display time. If the data loads in 210ms, you might still show the spinner for, say, a full 300ms. Remember to also be smart about your spinner style and context. Is it a subtle loading indicator in a button, or a full-page blocker? If it's a long process, progress bars are way better than just a spinner.

Improving User Experience with Better Performance

Performance is key to creating a good user experience. A slow app makes users frustrated and more likely to leave. Here are some practical ways to make your frontend faster and easier to use:

1. Lazy Loading

Load images, components, or code only when they’re needed. For example, use dynamic imports to split your code into smaller chunks that load on demand. This reduces the time users spend waiting for a page to load.

2. Pagination and Infinite Scrolling

When dealing with large datasets, don’t load everything at once. Use pagination to show one page of results at a time or infinite scrolling to load more content as the user scrolls down. This reduces the load on the browser and speeds up page rendering.

3. Virtualization

For long lists or grids, only render items that are currently visible in the viewport. Libraries like react-window or react-virtualized make this easier. This reduces memory use and keeps scrolling smooth.

4. Debouncing and Throttling

For actions like typing in a search box or handling scroll events, limit how often the function runs by using debouncing or throttling. This keeps your app responsive without overwhelming the browser.

5. Preloading and Prefetching

Load key resources, such as fonts or above-the-fold images, ahead of time so they’re ready when the user needs them. Prefetch resources for the next page based on what the user is likely to do next to make navigation faster.

7. Optimizing Images and Assets

Compress images to reduce their file size and use formats like WebP or AVIF. Serve the right size of images for different devices with the srcset attribute. Minify your CSS, JavaScript, and HTML to make your app load faster.

8. Monitoring and Testing

Use tools like Chrome DevTools, Lighthouse, or React Profiler to spot and fix slow parts of your app. Test your app on different devices and network speeds to ensure it performs well for everyone.

Building Forms for a Better User Experience

A great user experience requires a good form design, since it's one of the most important way users interact with the app. Use clear form labels, appropriate input types (e.g., type="email", type="number"), real time form validation, and make sure you show meaningful error messages.

Also prevent double submissions by disabling the button and showing some processing indicator after a form is submitted.

Conclusion

UX is not a separate concern; it's an integral part of development. As frontend developers, we need to proactively think about how users will interact with our work. By implementing these tactics, you can significantly improve user experience, make users happier, and create a more polished web application.