Why I created

Andrey Sitnik, Evil Martians

@sitnikcode

product development consultancy

I created at Evil Martians

Autoprefixer

PostCSS

Browserslist

@sitnikcode

Autoprefixer was started in 2013

@sitnikcode

Now Autoprefixer and PostCSS grew up

@sitnikcode

Image: CLM BBDO Paris

They don’t need new features

@sitnikcode

It is a time for а new adventure

Image: Fallout 1

Part 1. The problem.

Image: Invasion Base on the Moon (Apr, 1948)

The 36-Hour War: Life Magazine, 1945

Front-end and back-end are great

@sitnikcode

image/svg+xml
image/svg+xml

Back-end

Front-end

RoR, Django

Rust, Go, Node.js

Serverless

Svelte

The problem is hidden between them

@sitnikcode

image/svg+xml
image/svg+xml

Back-end

Front-end

Communication

AJAX is enouph, isn’t it?

@sitnikcode

fetch('/comments.json')
  .then(response => response.json())
  .then(list => {
    setComments(list)
  })

Simple AJAX works only on localhost

@sitnikcode

Image author: Nikita Prokopov

AJAX wasn’t created for the real Internet

@sitnikcode

Image author: Nikita Prokopov

A lot of code even for simple edge cases

@sitnikcode

+ showLoader()
  fetch('/comments.json')
    .then(response => response.json())
    .then(list => {
+     hideLoader()
      setComments(list)
    })
+  .catch(err => {
+     hideLoader()
+     if (isNoNetwork(err)) {
+       showNetworkError(err)
+     } else {
+       showServerError(err)
+     }
+   })

Developers are lazy to think about edge cases

and it is OK

@sitnikcode

Bad UX on real Internet is everywhere

@sitnikcode

There is no lazy developers,
there is only bad DX

Part 2. The dream.

Image: Robert McCall

Let’s forget about AJAX…

fetch('/comments.json')
fetch('/comments.json')
fetch('/comments.json')

@sitnikcode

and dream about the perfect state sync for SPA

@sitnikcode

We have stores in our apps

import { comments } from './stores.js'

function onLike (commentId) {
  comments.update(state => {
    state[commentId].like = !state[commentId].like
  })
}

@sitnikcode

With extra code to sync state with server 😒

import { comments } from './stores.js'

async function onLike (commentId) {
  loading = true
  try {
    await fetch(`/like?comment=${ commentId }`)
  } finally {
    loading = false
  }
  comments.update(state => {
    state[commentId].like = !state[commentId].like
  })
}

@sitnikcode

Feature 1: sync state without extra code

comments[10].text = 'A'
+----+------+
| id | text |
+----+------+
| 10 | A    |
+----+------+

@sitnikcode

Feature 2: sync state between tabs by default

3

3

@sitnikcode

Feature 3: built-in authorization

@sitnikcode

{
  userId: 380,
  token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMj' +
         'M0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2M' +
         'jM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
}

Feature 3a: built-in client version

@sitnikcode

API

Front-end

Client

Version 1

Version 1

Version 1

Developer

Load

Deploy

Version 2

Version 2

API call

API call

Version 1

Version 1

Version 2

Feature 4: Optimistic UI

Continue to work

Save

Continue to work

Save

@sitnikcode

You need to be pessimistic

to make good Optimistic UI

Offline: keep changes,

             show warning in global UI

Server error: revert changes,
                     show error in global UI

@sitnikcode

Feature 4a: global UI for network errors

@sitnikcode

Feature 4b: Offline

@sitnikcode

Expectations

Reality

no stable Internet in subway

Feature 5: Collaborative First Apps

@sitnikcode

Even single user apps have multiple “users”

@sitnikcode

1

2

@sitnikcode

Feature 5a: Real-Time Updates

changes

Feature 5b: The same changes order

@sitnikcode

Rename to A

Final: B

Rename to B

Rename to B

Final: A

Rename to A

Part 3. Implementations.

My dream

@sitnikcode

  1. Sync global state without extra code
  2. Cross-tab sync out of the box
  3. Authorization and client versions
  4. Optimistic UI
    1. Global UI for errors
    2. Offline First
  5. Collaborative First
    1. Real-Time
    2. Conflicts Resolution

Current solutions

Sync state

Cross-tab

Auth & versions

Optimistic UI

Collaborative

GraphQL

Firebase

Automerge

@sitnikcode

We often forget about these important criterias

@sitnikcode

Size

Vendor lock-in

GraphQL

Firebase

Automerge

219 KB

60 KB

25 KB

Part 4. The reality.

My own solution for client-server sync

@sitnikcode

Proxy server for Web Sockets

@sitnikcode

WebSocket

HTTP

Your back-end

Redux API on client-side

- import { createStore } from 'redux'
+ import createLoguxCreator from '@logux/redux/create-logux-creator'

+ const createStore = createLoguxCreator({
+   subprotocol: '1.0.0',
+   server: 'wss://logux.example.com',
+   userId: localStorage.userId,
+   credentials: localStorage.userToken
+ })

const store = createStore(reducer, preloadedState, enhancer)
+ store.client.start()

only 12 KB

@sitnikcode

MobX and svelte/store APIs are coming

@sitnikcode

Auth and client versioning

const createStore = createLoguxCreator({
  server: url,
  subprotocol: '1.0.0',
  userId: localStorage.userId,
  credentials: localStorage.userToken
})

@sitnikcode

Sync state with server and tabs

- store.dispatch({
+ store.dispatch.sync({
    type: 'users/add',
    user
  })

@sitnikcode

Optimistic and Pessimistic UI

// Optimistic UI
store.dispatch.sync(addUser)

// Pessimistic UI
loader = true
await store.dispatch.sync(addUser)
loader = false

@sitnikcode

Changes reverting and global UI for errors

import badge from '@logux/client/badge'

@sitnikcode

Logux Proxy re-send changes to clients

@sitnikcode

WebSocket

HTTP

WebSocket

Changes have time mark to keep order

@sitnikcode

store.log.on('action', (action, meta) => {
            // Distributed time
  meta.id   //=> [1489328408677, '10:bfj58s0w:au68-dd', 0]
            // Local time on this node
  meta.time //=>  1489328404561
})

@sitnikcode

Part 5. Future

Logux solves my dreams

@sitnikcode

Sync state

Cross-tab

Auth & versions

Optimistic UI

Collaborative

Logux

Questions?

Why I created Logux

By Andrey Sitnik

Why I created Logux

  • 2,937