React
Install the package and start it from one file imported by your entry point.
Installed with the @userlily/js npm package · Written for React 19 with Vite
What works
| Product | Status | Note |
|---|---|---|
| Analytics & custom events | Supported | |
| Feedback widget | Supported | |
| Review badge | Supported | |
| Web push | Supported | |
| Roadmap embed | Supported | |
| Feedback, survey & roadmap links | Supported |
1. Install the package
Terminal
npm install @userlily/js
2. Add your keys
Vite only exposes variables starting with VITE_ to the browser. They are built in - rebuild after changing them.
.env
VITE_USERLILY_HOST=https://dev.userlily.com VITE_USERLILY_TRACKING_ID=YOUR_TRACKING_ID VITE_USERLILY_CAMPAIGN=YOUR_CAMPAIGN_KEY VITE_USERLILY_BROADCAST_KEY=YOUR_PROJECT_KEY
3. Create the userlily file
A module runs once, however often it is imported - so this starts every product exactly once. Delete the products you do not use.
src/userlily.js
import { init } from '@userlily/js'
init({
host: import.meta.env.VITE_USERLILY_HOST,
analytics: {
trackingId: import.meta.env.VITE_USERLILY_TRACKING_ID,
},
widget: {
campaign: import.meta.env.VITE_USERLILY_CAMPAIGN,
},
broadcast: {
project: import.meta.env.VITE_USERLILY_BROADCAST_KEY,
},
})4. Import it from your entry point
Before the app renders. React Router navigation is recorded automatically.
src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './userlily'
import App from './App'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)5. Send custom events
src/SignupButton.jsx
import { track } from '@userlily/js'
export default function SignupButton() {
return (
<button onClick={() => track('signup_completed')}>
Sign up
</button>
)
}6. Show the review badge
Place <ReviewBadge /> where it should appear.
src/ReviewBadge.jsx
// src/ReviewBadge.jsx
import { useEffect, useRef } from 'react'
import { mountSyndication } from '@userlily/js'
export default function ReviewBadge() {
const ref = useRef(null)
useEffect(() => {
// Returns a cleanup that removes the badge again.
return mountSyndication({
host: import.meta.env.VITE_USERLILY_HOST,
campaign: 'YOUR_CAMPAIGN_KEY',
target: ref.current,
})
}, [])
return <div ref={ref} />
}7. Host the push worker
The worker must be served from your own domain at /userlily-sw.js, as JavaScript, with no redirect and over HTTPS. Each of these fails silently. Vite serves public/ at the site root.
Terminal - re-run after updating the package
npx --no userlily copy-sw public
8. Ask for push permission from your own button
Optional. Resolves false when the prompt cannot be shown.
src/NotifyButton.jsx
import { broadcast } from '@userlily/js'
// Shows the opt-in when "When to ask" is set to
// "Only when your own code asks".
export default function NotifyButton() {
return (
<button onClick={() => broadcast.prompt()}>
Get notified
</button>
)
}9. Embed the public roadmap
Turn on “Enable public roadmap” in Roadmap → Settings first.
src/RoadmapEmbed.jsx
import { useEffect, useRef } from 'react'
const ORIGIN = 'https://dev.userlily.com'
export default function RoadmapEmbed() {
const ref = useRef(null)
useEffect(() => {
function onMessage(event) {
if (event.origin !== ORIGIN) return
const data = event.data
if (data?.type !== 'userlily:roadmap:height') return
if (ref.current && data.height > 0) {
ref.current.style.height = data.height + 'px'
}
}
window.addEventListener('message', onMessage)
return () => window.removeEventListener('message', onMessage)
}, [])
return (
<iframe
ref={ref}
src={ORIGIN + '/r/YOUR_ROADMAP_KEY?embed=1'}
title="Product roadmap"
loading="lazy"
style={{ width: '100%', height: 720, border: 0 }}
/>
)
}10. Share links
Every campaign, survey and public roadmap also has a hosted page - nothing to install. Find each one under Share in the dashboard.
Hosted pages
Feedback form https://dev.userlily.com/f/YOUR_CAMPAIGN_KEY Survey https://dev.userlily.com/s/YOUR_SURVEY_KEY Roadmap https://dev.userlily.com/r/YOUR_ROADMAP_KEY
Notes
- Next.js, Remix and other server-rendering React frameworks: see the Next.js guide -
init()must run in the browser, from an effect.
Every option, limit and troubleshooting step is in the main docs. Logos are trademarks of their owners and are shown only to indicate compatibility.