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
Back-end
Front-end
RoR, Django
Rust, Go, Node.js
Serverless
Svelte
The problem is hidden between them
@sitnikcode
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
- Sync global state without extra code
- Cross-tab sync out of the box
- Authorization and client versions
-
Optimistic UI
- Global UI for errors
- Offline First
-
Collaborative First
- Real-Time
- 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,921