UseSyncExternalStore hook with FireStore
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 firebase # react-hook-form is for forms and inputs handling in react > npm install react-hook-form
-
Create files
> touch src/firease.js > touch src/store.js
-
Styling is applied using tailwindcss
Firebase initialization in firease.js
:
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
const firebaseConfig = {
// TODO: Add your firebase configs here
}
export const app = initializeApp(firebaseConfig)
export const db = getFirestore(app)
Create a store with Firestore in store.js
:
import { db } from './firebase'
import {
collection,
onSnapshot,
query,
orderBy,
addDoc,
serverTimestamp,
setDoc,
doc,
} from 'firebase/firestore'
export default function externalStore() {
// Store logic and functions
const addTodo = async (todo) => {
try {
await addDoc(collection(db, 'todos'), {
name: todo,
isCompleted: false,
createdAt: serverTimestamp(),
})
} catch (e) {
console.error('Error adding document: ', e)
}
}
const completeTodo = async (id, isCompleted) => {
try {
await setDoc(doc(db, 'todos', id), { isCompleted }, { merge: true })
} catch (e) {
console.error('Error updating document: ', e)
}
}
// Data
let todos = []
// Subscribe function for React sync external store hook
const subscribe = (listner) => {
return onSnapshot(
query(collection(db, 'todos'), orderBy('createdAt')),
(querySnapshot) => {
// Update data
todos = querySnapshot.docs.map((doc) => {
return {
id: doc.id,
...doc.data(),
}
})
// Call listener function after data updates
listner()
}
)
}
// 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 todos
}
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
Note: You will need to provide Firebase configuration to make it work