Before you start, make sure you had a look at the Simple React Todos App
.
If you understood the simple example, you are prepared to go a step further.
We are going to solve the same problem with the same outcome from a user's perspective, but with an entirely different implementation.
If you take a step back and look at the design of the simple example you recognize that our backend class reflects something like a manager for our todo items. The Todo.js backend-class provides indirect access to the items through an index generated by the backend. The individual todo items can not directly be accessed.
While this is completely fine here, you could think of more sophisticated cases, where the corresponding thing to the todo item may not only carry data information but posses much more business logic, inner-state, and life-cycle. In such a case a manager will quickly become tedious and complex to write as it must wrap all added functionality.
With this in mind we start the second example. As before we first create a new
directory, (e.g. vrpc-react-todos-2
) and run:
mkdir backend
cd backend
npm init -f -y
npm install vrpc
mkdir src
in there.
As we want to directly control Todo
items instead of doing this indirectly
through a manager, we write our backend like this:
src/Todo.js
const EventEmitter = require('events')
class Todo extends EventEmitter {
constructor (text) {
super()
this._data = { text, completed: false }
}
getData () {
return this._data
}
toggleCompleted () {
this._data.completed = !this._data.completed
this.emit('update', this._data)
}
}
module.exports = Todo
So, what we have done is writing a class whose instances directly reflect
individual Todo
items. Moreover, have we added a new capability of emitting
an update
event whenever the item's state is changed (mutated).
Again, we register this class through VRPC to make it remotely callable:
index.js
const { VrpcAdapter, VrpcAgent } = require('vrpc')
// Register class "Todos" to be remotely callable
VrpcAdapter.register(require('./src/Todo'))
async function main () {
try {
const vrpcAgent = new VrpcAgent({
agent: 'example-advanced-todos-agent',
domain: 'vrpc'
})
await vrpcAgent.serve()
} catch (err) {
console.log('VRPC triggered an unexpected error', err)
}
}
// Start the agent
main()
Now, it's interesting to see how it can be used via VRPC
Live. Select vrpc
as domain and you will see the Todo
class under your agent name.
As VRPC can itself manage the life-time of instances (through the injected
methods create
and delete
) you can create a Todo
item by calling create
with the argument keep coding
.
This will create a shared instance of the Todo
class and pass 'keep coding'
to the constructor.
Once created, you can call all member functions. In the frontend implementation we will make use of this VRPC feature.
Let's quickly setup the project by running the following lines (from within the root directory):
npx create-react-app frontend
cd frontend
npm install vrpc
npm install react-vrpc
mkdir -p src/components
and modifying the index.js file:
src/index.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './components/App'
import * as serviceWorker from './serviceWorker'
import { createVrpcProvider } from 'react-vrpc'
const broker = process.env.REACT_APP_BROKER_HOST
? `ws://${process.env.REACT_APP_BROKER_HOST}:8080/mqtt`
: 'wss://vrpc.io/mqtt'
const VrpcProvider = createVrpcProvider({
broker,
domain: 'vrpc',
backends: {
todos: {
agent: 'example-advanced-todos-agent',
className: 'Todo'
}
}
})
const root = createRoot(document.getElementById('root'))
root.render(
<VrpcProvider>
<App />
</VrpcProvider>
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
Note the difference to the simple example! We are specifying agent
and className
but NOT instance
and/or args
.
This makes the todos
backend a manager for its own instances.
src/components/App.js
import React from 'react'
import AddTodo from './AddTodo'
import ShowTodos from './ShowTodos'
import { useBackend } from 'react-vrpc'
export default function App () {
const [, error] = useBackend('todos')
if (error) return `Error! ${error.message}`
return (
<>
<h1>Todo List</h1>
<AddTodo />
<ShowTodos />
</>
)
}
This file is identical to the one in the simple example, so quickly to the next.
src/components/AddTodo.js
import React from 'react'
import { useBackend } from 'react-vrpc'
export default function AddTodo () {
const [todos] = useBackend('todos')
function handleSubmit (e) {
e.preventDefault()
const { value } = input
input.value = ''
if (!value.trim()) return
const id = Date.now().toString()
todos.create(id, { args: [value] })
}
let input
return (
<div>
<form onSubmit={handleSubmit}>
<input ref={node => (input = node)} />
<button type='submit'>Add Todo</button>
</form>
</div>
)
}
Here, you can see the managing aspect of the todos
backend. The two lines:
const id = Date.now().toString()
await backend.create(id, { args: [value] })
are the important ones. First, we create an id
(using a timestamp), then
we create a new instance (on the backend!) of a Todo
class by means of the
create
method.
Not used here, but just for completeness: there is a get(id)
and delete(id)
method available as well.
When a new instance is created, all todos
backends will automatically be
updated, so no need to explicitly call refresh()
here.
src/components/ShowTodos.js
import React, { useState } from 'react'
import { useBackend } from 'react-vrpc'
import TodoItem from './TodoItem'
import Filter from './Filter'
export default function ShowTodos () {
const [filter, setFilter] = useState('all')
const [{ ids }] = useBackend('todos')
return (
<div>
<ul>
{ids.map(id => (
<TodoItem key={id} id={id} filter={filter} />
))}
</ul>
<Filter onClick={setFilter} filter={filter} />
</div>
)
}
This component got much shorter, as we moved more logic to the TodoItem
.
Its main purpose is rendering out the list while applying the correct filter
to it.
Note, how the available id
s can be retrieved from the todos
backend:
const { ids } = todos
and that it does not matter what their values are (as long as they are unique).
src/components/TodoItem.js
import React, { useEffect, useState } from 'react'
import { useBackend } from 'react-vrpc'
export default function TodoItem ({ id, filter }) {
const [todo] = useBackend('todos', id)
const [data, setData] = useState({ text: '', completed: false })
useEffect(() => {
if (!todo) return
todo.getData().then(data => setData(data))
const handleUpdate = data => setData(data)
todo.on('update', handleUpdate)
return () => {
todo.off('update', handleUpdate).catch(() => {})
}
}, [todo])
function handleClick () {
todo.toggleCompleted()
}
if (filter === 'completed' && !data.completed) return null
if (filter === 'active' && data.completed) return null
return (
<li
onClick={handleClick}
style={{ textDecoration: data.completed ? 'line-through' : 'none' }}
>
{data.text}
</li>
)
}
This component exactly reflects a single instance of the Todo
class, you
could say it is its "React-Component-Twin".
The nicety about this design is that the logic can stay in the lowest hierarchy component, hence minimizing re-rendering and maximizing performance.
The magic starts with:
const [todo] = useBackend('todos', id)
which provides us with a proxy (todo
) to a specific Todo
instance of the
backend.
The rest of the code is obvious besides one feature that is new here:
useEffect(() => {
if (!todo) return
todo.getData().then(data => setData(data))
const handleUpdate = data => setData(data)
todo.on('update', handleUpdate)
return () => {
todo.off('update', handleUpdate).catch(() => {})
}
}, [todo])
This effect hook's purpose is to make the UI automatically update whenever the backend is updated - even when done through a third party.
You can see that it works by simply starting a second UI in a another browser tab and watching them synchronizing each other nicely, or you manipulate the backend using VRPC Live.
IMPORTANT
Don't be afraid using events with react-vrpc, it actually is the recommended way to synchronize your frontend state with the backend state.
The VRPC API fully implements NodeJS'
event
module, just remember to make your backend an event emitter by inheriting the EventEmitter class.
The last component is boring and did not change from the simple example:
src/components/Filter.js
import React from 'react'
export default function Filter ({ onClick, filter }) {
return (
<div>
<span>Show: </span>
<button
onClick={() => onClick('all')}
disabled={filter === 'all'}
style={{ marginLeft: '4px' }}
>
All
</button>
<button
onClick={() => onClick('active')}
disabled={filter === 'active'}
style={{ marginLeft: '4px' }}
>
Active
</button>
<button
onClick={() => onClick('completed')}
disabled={filter === 'completed'}
style={{ marginLeft: '4px' }}
>
Completed
</button>
</div>
)
}
Voila, there are we again!
Application finished and working, thanks for reading, enjoy playing!