UseSyncExternalStore hook with rxjs
React 18 has added multiple new hooks to its arsenal, useSyncExternalStore
is generally underused. It is useful to integrate with existing non React code like any data store library, external and changing data source. Let’s see how to implement it with real-life examples.
Specification
const snapshot = useSyncExternalStore(subscribe, getSnapshot)
To connect to the external store this hook expects 2 functions from the external store.
subscribe
: This function takes the listener function as input and returns a function that unsubscribes.
getSnapshot
: This function should return the current snapshot of the data when called.
How to implement?
Let’s implement useSyncExternalStore with Firestore.
Setup
-
Create react project using
create-react-app
> npx create-react-app my-app > cd my-app
-
Install dependencies
> npm install rxjs # react-hook-form is for forms and inputs handling in react > npm install react-hook-form # nanoid is for unique id generation > npm install nanoid
-
Create files
> touch src/store.js
-
Styling is applied using tailwindcss
Create a store with rxjs in store.js
:
import { BehaviorSubject } from 'rxjs'
import { nanoid } from 'nanoid'
export default function externalStore() {
const subject = new BehaviorSubject([]) // [] is initial empty ToDos list
// Store logic and functions
const addTodo = async (name) => {
const todo = {
id: nanoid(),
name,
createdAt: new Date().getTime(),
isCompleted: false,
}
subject.next([...subject.value, todo]) // Return newly created object
}
const completeTodo = async (id, isCompleted) => {
const todo = subject.value.find((val) => val.id === id)
todo.isCompleted = isCompleted
subject.next([...subject.value]) // Return newly created object
}
// Subscribe function for React sync external store hook
const subscribe = (listener) => {
const subscription = subject.subscribe(listener)
return () => subscription.unsubscribe()
}
// This function will get called at any time within the render cycle.
// But it mostly gets executed after the data update is notified through subscribe
// IMPORTANT: do not return a new object, else React will go into infinite render
const getSnapshot = () => {
return subject.value
}
return {
subscribe,
getSnapshot,
addTodo,
completeTodo,
}
}
Use store within the component in App.js
:
import './App.css'
import { useSyncExternalStore } from 'react'
import { useForm } from 'react-hook-form'
import externalStore from './store'
// NOTE: Store is initialized outside the React component
const store = externalStore()
function App() {
// Attach Custom Store with component
const todos = useSyncExternalStore(store.subscribe, store.getSnapshot)
// Handle component specific logic
const { register, handleSubmit } = useForm()
const onSubmit = (data) => {
store.addTodo(data.todo)
}
const handleComplete = (id, isCompleted) => {
store.completeTodo(id, isCompleted)
}
return (
<div>
<h1>TODOs</h1>
<div className="flex flex-col space-y-2">
{todos &&
todos.map((todo) => (
<Todo key={todo.id} todo={todo} onChange={handleComplete} />
))}
</div>
<form
className="mt-2 flex flex-row space-x-2"
onSubmit={handleSubmit(onSubmit)}
>
<label>Add To Do</label>
<input type="text" {...register('todo', { required: true })}></input>
<button type="submit">Add</button>
</form>
</div>
)
}
function Todo({ todo, onChange }) {
return (
<div>
<input
type="checkbox"
id={todo.id}
name={todo.id}
onChange={(evt) => {
onChange(evt.target.id, !(evt.target.value === 'true'))
}}
checked={todo.isCompleted}
value={todo.isCompleted}
></input>
<label htmlFor={todo.id}>{todo.name}</label>
</div>
)
}
export default App
Find implementation with codesandbox here