react-feed-system
v0.1.5
Published
Customizable React feed, comments, reactions, and backend adapter system.
Maintainers
Readme
react-feed-system
Customizable React feed, comments, replies, reactions, filters, and backend adapters.
react-feed-system is not tied to one domain. It can power feeds for schools,
universities, companies, clubs, communities, SaaS apps, or internal tools. The
package uses generic audience targeting:
segments: broad groups such as sections, teams, departments, houses, regions, or offices.levels: nested levels such as classes, years, grades, cohorts, seniority, or phases.attributes: any extra targeting values your app needs.
The core is backend-agnostic. A Firebase adapter is included, but you can wire
REST, GraphQL, Supabase, local mocks, or any custom backend with
createHandlerFeedAdapter.
Install
npm install react-feed-systemReact is a peer dependency. Firebase is optional and only needed for
react-feed-system/adapters/firebase.
Quick Start With Custom Handlers
import { Feed } from 'react-feed-system';
import 'react-feed-system/styles.css';
import { createHandlerFeedAdapter } from 'react-feed-system/adapters/handlers';
const adapter = createHandlerFeedAdapter({
listEntries: async () => ({
entries: await api.feed.list(),
viewerReactions: await api.feed.myReactions(),
}),
createEntry: (data) => api.feed.create(data),
castReaction: (entryId, reaction) => api.feed.react(entryId, reaction),
deleteEntry: (entryId) => api.feed.delete(entryId),
});
export function AppFeed() {
return (
<Feed
adapter={adapter}
viewer={{
id: currentUser.id,
name: currentUser.name,
roles: currentUser.roles,
segments: currentUser.teams,
levels: currentUser.cohorts,
attributes: {
office: currentUser.office,
region: currentUser.region,
},
}}
/>
);
}Firebase Setup
The Firebase adapter does not initialize Firebase. Pass your existing Firebase instances.
import { Feed } from 'react-feed-system';
import 'react-feed-system/styles.css';
import { createFirebaseFeedAdapter } from 'react-feed-system/adapters/firebase';
import { db, functions, rtdb, storage } from './firebase';
const adapter = createFirebaseFeedAdapter({
firestore: db,
realtimeDatabase: rtdb,
storage,
functions,
callableName: 'feedHandler',
});
export function FirebaseFeed() {
return (
<Feed
adapter={adapter}
viewer={{
id: user.uid,
name: profile.name,
roles,
segments: profile.groups,
levels: profile.levels,
attributes: profile.feedAttributes,
}}
config={{
options: {
segments,
levels,
},
}}
/>
);
}The Firebase adapter defaults are compatible with the current NEAUXSIS backend shape by mapping generic package fields to the existing callable payload:
targetAudience.key-> backend audience type field.targetAudience.values.segment-> backend segment field.targetAudience.values.levels-> backend levels field.
You can override field names or callable action names:
createFirebaseFeedAdapter({
firestore: db,
functions,
collectionPath: 'communityFeed',
commentsPath: (entryId) => `comments/${entryId}`,
schema: {
audienceTypeField: 'audienceType',
segmentField: 'groupId',
levelsField: 'levelIds',
},
actionNames: {
castReaction: 'reactToEntry',
},
});Audience Targeting
Audience targeting is field-driven. Define whatever fields your app needs.
<Feed
adapter={adapter}
viewer={{
id: viewer.id,
roles: viewer.roles,
segments: viewer.teams,
levels: viewer.cohorts,
attributes: {
office: viewer.office,
region: viewer.region,
},
}}
config={{
audiences: [
{ key: 'everyone', label: 'Everyone' },
{
key: 'team',
label: 'Team',
fields: [
{ key: 'segment', label: 'Team', optionsKey: 'teams', required: true },
],
},
{
key: 'office_region',
label: 'Office and Region',
fields: [
{ key: 'office', label: 'Office', optionsKey: 'offices', required: true },
{ key: 'region', label: 'Region', optionsKey: 'regions', required: true },
],
},
],
options: {
teams: ['Design', 'Engineering', 'Support'],
offices: ['Kolkata', 'Delhi'],
regions: ['East', 'North'],
},
}}
/>School Example
For a school, classes and sections are just audience fields.
<Feed
adapter={adapter}
viewer={{
id: student.id,
name: student.name,
roles: ['member'],
attributes: {
classLevel: student.classLevel,
section: student.section,
},
}}
config={{
audiences: [
{ key: 'everyone', label: 'Everyone' },
{
key: 'class_section',
label: 'Class and Section',
fields: [
{ key: 'classLevel', label: 'Class', optionsKey: 'classes', required: true },
{ key: 'section', label: 'Section', optionsKey: 'sections', required: true },
],
},
{
key: 'staff_only',
label: 'Teachers and Admin',
canTarget: (viewer) => viewer.roles?.some((role) => ['teacher', 'admin'].includes(role)) ?? false,
visibleWhen: (_entry, viewer) => viewer.roles?.some((role) => ['teacher', 'admin'].includes(role)) ?? false,
},
],
options: {
classes: ['6', '7', '8', '9', '10', '11', '12'],
sections: ['A', 'B', 'C'],
},
}}
/>University Example
Domain language belongs in consumer config. A university can label the generic segment/level fields however it wants.
<Feed
adapter={adapter}
viewer={{
id: user.id,
name: user.name,
roles: user.roles,
segments: user.department ? [user.department] : [],
levels: user.semester ? [user.semester] : [],
}}
config={{
audiences: [
{ key: 'everyone', label: 'Everyone' },
{
key: 'segment',
label: 'Department',
fields: [
{ key: 'segment', label: 'Department', optionsKey: 'segments', required: true },
],
},
{
key: 'segment_level',
label: 'Department and Semester',
fields: [
{ key: 'segment', label: 'Department', optionsKey: 'segments', required: true },
{ key: 'levels', label: 'Semesters', optionsKey: 'levels', multiple: true, required: true },
],
},
],
options: {
segments: departmentOptions,
levels: semesterOptions,
},
}}
/>Custom UI
Use CSS variables for theming:
.my-feed {
--rfs-color-primary: #0f766e;
--rfs-color-surface: #ffffff;
--rfs-radius: 6px;
}Use render overrides when you want full control over specific pieces:
<Feed
adapter={adapter}
viewer={viewer}
className="my-feed"
config={{
renderers: {
entryBody: ({ entry }) => (
<section>
<h3>{entry.authorName}</h3>
<p>{entry.text}</p>
</section>
),
reactionButtons: ({ entry, onReact }) => (
<button onClick={() => onReact('upvote')}>
Applaud {entry.upvoteCount ?? 0}
</button>
),
},
}}
/>Permissions
Audience targeting and issue visibility can use different policies. For example,
an app can hide Everyone from notification targeting while still allowing it as
an issue visibility choice.
<Feed
adapter={adapter}
viewer={viewer}
config={{
permissions: {
canTargetAudience: (viewer, audience) => (
audience.key !== 'everyone' && viewer.roles?.includes('staff')
),
canSetVisibility: (viewer, audience, entryType) => (
entryType === 'issue' &&
['everyone', 'staff_only'].includes(audience.key) &&
viewer.roles?.includes('staff')
),
},
}}
/>Headless Usage
import {
FeedSystemProvider,
useFeed,
useFeedActions,
} from 'react-feed-system';
function CustomFeedList() {
const { entries, loading } = useFeed();
const { reactToEntry } = useFeedActions();
if (loading) return <div>Loading...</div>;
return entries.map((entry) => (
<article key={entry.id}>
<p>{entry.text}</p>
<button onClick={() => reactToEntry(entry.id, 'upvote')}>Like</button>
</article>
));
}
export function CustomFeed() {
return (
<FeedSystemProvider adapter={adapter} viewer={viewer}>
<CustomFeedList />
</FeedSystemProvider>
);
}