Optimistic UX

2022
(he/him)

User Interface

User Experience

Usability

{...design}
{...design}

UI

UX

Usability

✨

Multi-Page

Single-Page

Hybrid

Server

renders

static

data source

Browser

renders

all

all

Traditional Multi-Page Apps

click
mutation event
mutation request
response
update content
woohoo!

Age of Single Page Applications

click
mutation event
mutation request
response
update content
woohoo!

Don’t

go

chasing

waterfalls

πŸ’­ this song is from 1995

Hybrid Apps with Optimistic

click
mutation event
mutation request
response
update content
woohoo!
βœ…

β€œExpect...

...the unexpected”

those 1 every 999

click
mutation event
mutation request
πŸ’₯
update content
woohoo!
update content
Pardon. retry?

state chart

important notes

if your App/API fails often, you probably have a bug

not a silver bullet

πŸ›

πŸ”«

then

when?

when the response is not critical to the user experience

when you trust the availability of the resource

🐢

β˜€οΈ

loader function

when a page loads

`GET`

requests

⬇️

βš™οΈ

action function

when a data mutates

`POST`

requests

⬆️

βš™οΈ

`PUT`
`DELETE`
`PATCH`
`useLoaderData()`
`useActionData()`

useTransition()

GET form submission
POST / PUT / PATCH / DELETE form submission

from remix

not react

⚠

navigation
idle
idle
loading
idle
idle
submitting
idle
idle
submitting
loading

1️⃣

2️⃣

3️⃣

const PageComponent = () => {
  const loaderData = useLoaderData()
  const transition = useTransition()

  return (
    <h1>
      {transition.state === 'submitting'
        ? `Submitted ${transition.submission.formData.get('username')}`
        : `Already have ${loaderdata?.username || 'nothing'}`
       }
    </h1>
   )
 }

routes/page.jsx

demo

time

<Form>
<fetcher.Form>

vs

<form>

vs

HTML5 Logo

πŸͺ

useFetcher()

This hook lets you plug your UI into your actions and loaders without navigating

-- from Remix docs

POST
PUT
DELETE
PATCH

Action

Function

Loader

Function

const PageComponent = () => {
  const { data, submission, state } = useFetcher()
  const loaderdata = useLoaderData()
  
  const formData = submission.formData

  const food = fetcher.data
    ? `${data.cuisine} ${data.dish}`
    : loaderdata.food

  return (
    <h1>
      {state === 'submitting'
        ? `We are eating ${formData.get('dish')} for dinner`
        : `We are eating ${food || 'nothing'} for dinner`
      }
    </h1>
 }

routes/page.jsx

demo

time

Tanstack Query

featuring

getServerSideProps
QueryClient
initialData

Hydrate page with data

cache query
mutate

βœ…

❌

Re-Render page w/ new data

update cache

Re-Render page w/ old data

pull from cache
warn user
function App({ Component, pageProps }) {
  const [queryClient] = useState(
    () => new QueryClient()
  )

  return (
    <QueryClientProvider client={queryClient}>
      {/*... app stuff goes here */}
    </QueryClientProvider>
  )
 }

pages/app.jsx

function Page({ pageData }) {
  const { data } = useQuery(
    ['data'],
    () => {
      return getData(user)
    },
    {
      initialData: pageData,
    }
  )

  return <Component />
}

pages/page.jsx

const getServerSideProps = async ({
  req,
  res
}) => {
  const user = await getUser()
 
  return {
    props: {
      pageData: await fetchData(user)
    },
  }
}

pages/page.jsx

const useAddTodo = (user) => {
  const queryClient = useQueryClient()

  return useMutation(
    (newTodo: TodoProps) => addTodo(newTodo, userEmail), {
      onMutate: async (newTodo) => {
        await queryClient.cancelQueries(['todos'])

        const previousTodos = queryClient.getQueryData(['todos'])

        queryClient.setQueryData(['todos'], (oldTodos => [
          newTodo,
          ...oldTodos,
        ])

        return { previousTodos }
      },
      onError: (err, _newTodo, context) => {
        queryClient.setQueryData(['todos'], context.previousTodos)
      },
      onSettled: () => {
        queryClient.invalidateQueries(['todos'])
      },
    })
  )
}

hooks/query.jsx

demo

time

thanks

being optimistic is cool

Made with Slides.com