All three technologies (Logux, AJAX, GraphQL) was created for communication between clients and server. AJAX and GraphQL are based on requests and responses. Logux is based on synchronizing state by synchronizing list of actions by WebSocket. However, there are many similar things between these technologies.
Similarities
Loading the Data from Client
In AJAX you send GET request to some URL and wait for response.
// containers/users.js
export default () => {
const [state, setState] = useState('loading')
useEffect(async () => {
try {
const response = await fetch('/users', { credentials: 'include' })
if (response.ok) {
const users = response.json()
setState(users)
} else {
throw new Error('HTTP error ' + response.code)
}
} catch {
setState('error')
}
})
if (state === 'loading') {
return <Loader />
} else if (state === 'error') {
return <Error />
} else {
return <Users users={state} />
}
}
<!-- views/UsersView.vue -->
<template>
<Loader v-if="state === 'loading'" />
<Error v-else-if="state === 'error'" />
<Users v-else :users="users" />
</template>
<script>
import { ref, watch } from 'vue'
export default {
name: 'UsersView',
setup () {
let state = ref('loading')
watch(async () => {
try {
const response = await fetch('/users', { credentials: 'include' })
if (response.ok) {
const users = await response.json()
state.value = users
} else {
throw new Error('HTTP error ' + response.code)
}
} catch {
state.value = 'error'
}
})
return { state }
}
}
</script>
In GraphQL (Apollo) you wrap your component to make request to single entry point.
// containers/users.js
export default () => {
return <Query query={
gql`{
users {
id,
name
}
}`
}>
{({ loading, error, users }) => {
if (loading) {
return <Loader />
} else if (error) {
return <Error />
} else {
return <Users users={users} />
}
}}
</Query>
}
<!-- views/UsersView.vue -->
<template>
<Loader v-if="loading" />
<Error v-else-if="error" />
<Users v-else-if="users" :users="users" />
</template>
<script>
import { useQuery, useResult } from '@vue/apollo-composable'
import gql from 'graphql-tag'
export default {
name: 'UsersView',
setup () {
let { result, loading, error } = useQuery(gql`
query getUsers {
users: {
id,
name
}
}
`)
let users = useResult(result, null, data => data.users)
return { users, loading, error }
}
}
</script>
Logux is focusing on live-updates. You are subscribing to the channel instead of requesting the data. Logux Server sends current users list in users/add
actions. Logux has global UI to show server errors and much more reliable for network problems. As result, you do not need to care about errors in this case.
// reducers/users.js
export default function (state = { }, action) {
if (action.type === 'users/add') {
return { ...state, [action.user.id]: action.user }
}
}
// containers/users.js
export default () => {
const isSubscribing = useSubscription(['users'])
const users = useSelector(state => state.users)
if (isSubscribing) {
return <Loader />
} else {
return <Users users={users} />
}
}
// store/users/mutations.js
export default {
…
'user/add': (state, action) => {
state.users = { ...state.users, [action.user.id]: action.user }
}
}
<!-- views/UsersView.vue -->
<template>
<Loader v-if="isSubscribing" />
<Users v-else :users="users" />
</template>
<script>
import {
computed,
useStore,
useSubscription
} from '@logux/vuex'
export default {
name: 'UsersView',
setup () {
let store = useStore()
let isSubscribing = useSubscription(['users'])
let users = computed(() => store.state.users)
return { isSubscribing, users }
}
}
</script>
Change the Data on the Client
In AJAX you send POST request with the new data:
// containers/user-form.js
export default ({ userId }) => {
const [state, setState] = useState()
const onNameChanged = useCallback(async name => {
setState('loading')
try {
const response = await fetch(`/users/${ userId }`, {
method: 'PUT',
credentials: 'include'
})
if (response.ok) {
setState('saved')
} else {
throw new Error('HTTP error ' + response.code)
}
} catch {
setState('error')
}
})
if (state === 'loading') {
return <Loader />
} else {
return <UserForm error={state === 'error'} onSubmit={onNameChanged} />
}
}
<!-- views/UserFormView.vue -->
<template>
<Loader v-if="state === 'loading'"/>
<UserForm
v-else
:error="state === 'error'"
@submit="onNameChanged"
/>
</template>
<script>
import { ref, toRefs } from 'vue'
export default {
name: 'UserFormView',
props: ['userId'],
setup (props) {
let { userId } = toRefs(props)
let state = ref('ok')
async function onNameChanged () {
state.value = 'loading'
try {
const response = await fetch(`/users/${userId.value}`, {
method: 'PUT',
credentials: 'include'
})
if (response.ok) {
state.value = 'saved'
} else {
throw new Error('HTTP error ' + response.code)
}
} catch {
state.value = 'error'
}
return { state, onNameChanged }
}
}
}
</script>
In GraphQL you call a mutation:
// containers/user-form.js
const CHANGE_NAME = gql`
mutation ChangeName($name: String!, $id: ID!) {
changeName(name: $name, id: $id) {
id
name
}
}
`
export default ({ userId }) => {
return <Mutation mutation={CHANGE_NAME}>
{(changeName, { data }) => {
if (data.loading) {
return <Loader>
} else {
return <UserForm
error={data.error}
onSubmit={name => changeName({ variables: { name, id: userId } })}
>
}
}}
</Mutation>
}
<!-- views/UserFormView.vue -->
<template>
<Loader v-if="loading" />
<UserForm v-else @submit="name => mutate({ variables: { name, id: userId } })" />
</template>
<script>
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
export default {
name: 'UserFormView',
props: ['userId'],
setup (props) {
let { mutate: changeName } = useMutation(gql`
mutation ChangeName($name: String!, $id: ID!) {
changeName(name: $name, id: $id) {
id
name
}
}
`)
return { changeName, userId: props.userId }
}
}
</script>
In Logux you create the Redux action by dispatch.sync
. Logux uses Optimistic UI by default, so you do not need a loader in this case.
// reducers/users.js
export default function (state = { }, action) {
if (action.type === 'users/rename') {
let id = action.userId
return { ...state, [id]: { ...state[id], name: action.name } }
}
}
// containers/user-form.js
export default ({ userId }) => {
const dispatch = useDispatch()
const onNameChanged = useCallback(name => {
dispatch.sync({ type: 'users/rename', userId, name })
})
return <UserForm onSubmit={onNameChanged} />
}
// store/users/mutations.js
export default {
…
'user/rename': (state, action) => {
let id = action.userId
state.users = { ...state.users, [id]: { ...state.users[id], name: action.name } }
}
}
<!-- views/UserFormView.vue -->
<template>
<user-form @submit="onNameChange">
</template>
<script>
import { toRefs } from 'vue'
import { useStore } from '@logux/vuex'
export default {
name: 'UserFormView',
props: ['userId'],
setup (props) {
let store = useStore()
let { userId } = toRefs(props)
return {
onNameChange (name) {
store.commit.sync({
type: 'users/rename',
userId: userId.value,
name
})
}
}
}
}
</script>