7 min read Guides React Native

How to Add a Feedback Board to a React Native App

By the end of this guide, your React Native app has a screen where users can read every feature request other people have already filed, upvote the ones they care about, add a comment, and submit their own — and everything they submit lands in a queue you can triage, move through statuses, and push into an issue tracker. The SDK ships the list, the card, the vote button, the submit form and the detail view, so most of the work here is deciding where the screen lives in your navigation and how you identify a user.

This is a public, votable board, not a support inbox and not a crash reporter. If what you want is a private "contact us" form, this is more machinery than you need.

What you are actually building

Three screens, in practice:

  • A list — feedback sorted by votes by default, with a built-in sort picker for votes, newest, oldest and most-commented, and pull-to-refresh.
  • A submit form — title, description, a category picker (feature request, bug report, improvement, other), an optional email field, and an optional mailing-list opt-in that only appears once an email is typed.
  • A detail view — the full item with its status and category badges, a vote button, and the comment thread.

Use all three as shipped, or drop to the hooks and build your own UI on the same data. Both paths are below.

Install

The React Native SDK is a layer on top of the JavaScript SDK, and it persists an identifier with AsyncStorage. All three are peer dependencies, so install them together:

npm install feedbackkit-react-native feedbackkit-js @react-native-async-storage/async-storage

On bare React Native, run your usual pod install for AsyncStorage's iOS side. The FeedbackKit package itself is TypeScript and React Native primitives, so there is nothing to link.

Configure the provider

Everything hangs off one context provider. Wrap it around whatever subtree needs feedback UI — in most apps that means above your navigation container, so a feedback screen anywhere in the tree can use it.

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { FeedbackProvider } from 'feedbackkit-react-native';
import { FEEDBACK_API_KEY } from './feedback/config';

export default function App() {
  const user = useCurrentUser();

  return (
    <FeedbackProvider
      apiKey={FEEDBACK_API_KEY}
      userId={user?.id}
      theme={{ primaryColor: '#6366F1' }}
    >
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </FeedbackProvider>
  );
}

apiKey and children are the only required props. userId, theme, locale, translations and baseUrl are optional; baseUrl only matters if you are pointing at something other than the hosted API.

Where the key goes

The project API key is a client-side credential by design — it is the same key the web embed puts in a page, and it travels as an X-API-Key header on every request. It is scoped to one project and it is not your admin login, so shipping it inside the app binary is the intended use.

That said, treat it as configuration, not as a literal typed into a component:

  • Read it from your existing build-time config layer (Expo's public env vars, a native build config, whatever you already use) and import it from one module. When you rotate the key you touch one file.
  • Use a separate project — and therefore a separate key — for your debug builds. Test submissions in the same board as real users is a mess you only make once.
  • Never put an admin bearer token in the app. The Bearer-authenticated endpoints are for your own tooling; the SDK never needs them.

You can regenerate a project's key from its settings page, but that invalidates the old one immediately and every shipped build still using it stops working. Plan a rotation around a release.

Render the board

The list screen is almost entirely props:

import { FeedbackList, FeedbackStatus } from 'feedbackkit-react-native';

export function FeedbackScreen({ navigation }) {
  return (
    <FeedbackList
      filterByStatus={FeedbackStatus.Approved}
      showAddButton
      onAddPress={() => navigation.navigate('SubmitFeedback')}
      onFeedbackPress={(feedback) =>
        navigation.navigate('FeedbackDetail', { id: feedback.id })
      }
    />
  );
}

filterByStatus is the decision worth thinking about. Leave it off and users see everything, including pending items nobody has looked at yet. Set it to FeedbackStatus.Approved and the board becomes curated: nothing appears until you approve it.

When showAddButton is set and there is at least one item, the component renders a floating action button. When the list is empty it renders a submit button inside the empty state instead — which matters, because passing your own emptyComponent replaces that whole empty state, submit button included. If you customise it, put your own entry point into it.

The submit and detail screens are similarly thin:

import { SubmitFeedbackView, FeedbackDetailView, useFeedback } from 'feedbackkit-react-native';

export function SubmitFeedbackScreen({ navigation }) {
  return (
    <SubmitFeedbackView
      onSubmit={() => navigation.goBack()}
      onCancel={() => navigation.goBack()}
    />
  );
}

export function FeedbackDetailScreen({ route }) {
  const { feedback, isLoading, error } = useFeedback(route.params.id);

  if (isLoading) return <Spinner />;
  if (error || !feedback) return <NotFound />;

  return <FeedbackDetailView feedback={feedback} />;
}

onSubmit receives the created feedback item, so you can navigate straight to its detail screen instead of going back, if that reads better in your app.

If the shipped components do not match your design system, ignore them and use the hooks. They carry the loading, error and mutation state so you only write the view:

const { feedbacks, isLoading, error, refetch, filter, setFilter } =
  useFeedbackList({ sort: FeedbackSort.Newest });

const { submit, isSubmitting, error: submitError, clearError } = useSubmitFeedback();
const { vote, unvote, isVoting } = useVote();
const { comments, addComment, isAdding } = useComments(feedbackId);

There is also useFeedbackKit(), which hands you the underlying client if you need a call the hooks do not wrap. For smaller adjustments, the provider takes a partial theme object — override primaryColor and leave the rest — or you can pass the exported darkTheme.

Where to mount it

Three patterns cover nearly every app:

  1. A settings row. The default choice, and the one to pick if you are unsure. A "Feature requests" row in your settings list pushing the feedback screen onto the existing stack. Zero navigation surgery, and the board stays discoverable without competing for attention.
  2. A modal. Good when feedback is contextual — a "suggest an improvement" affordance on the screen the user is complaining about. Put the submit screen in a modal group and pass initialCategory so the category is pre-selected:
    <Stack.Group screenOptions={{ presentation: 'modal' }}>
      <Stack.Screen name="SubmitFeedback" component={SubmitFeedbackScreen} />
    </Stack.Group>
  3. A tab. Only if community input is genuinely part of what your app is. A permanent tab is a promise that the board is active; an abandoned one with three stale items is worse than no tab at all.

Whichever you choose, keep the provider at the root. Mounting it per-screen rebuilds the client on every navigation and redoes the identifier load it performs on mount.

What happens after someone taps Submit

A submission arrives with a status of pending, and the person who filed it is automatically counted as its first voter — you do not need to cast that vote yourself, and a brand new item showing one vote is correct, not a bug.

From there you move it through the status list. Two of the six statuses close voting:

StatusVoting
pendingOpen
approvedOpen
in_progressOpen
testflightOpen
completedClosed
rejectedClosed

The SDK respects this without you doing anything — VoteButton checks the status and refuses to fire on completed or rejected items, so users never get a server error from tapping a button that should not have been tappable.

Vote counts are the point of a board rather than an inbox: they turn "three loud users" into a ranked list. Sorting by votes is the list's default, which also nudges people toward upvoting an existing item instead of filing a duplicate.

Once something is worth building, push it to where you actually work. FeedbackKit creates tickets in GitHub, Notion, ClickUp, Linear, Monday.com, Trello, Airtable, Asana and Basecamp, and posts notifications to Slack. Capabilities are not identical across providers — most support two-way status and comment sync, but GitHub does create and status sync only, with no comment sync. If a closed loop between tracker comments and board comments matters to you, check the integrations page before you commit to a provider.

Troubleshooting

"User ID is required to vote / submit feedback." This is the one people hit. Voting, submitting and commenting all need a user identifier, and the provider does not fabricate one — it loads a previously persisted identifier from AsyncStorage, and if there is none, userId stays undefined and those calls throw. Pass userId to the provider for signed-in users. For anonymous users, generate a stable identifier once and set it yourself:

import { useEffect } from 'react';
import { useFeedbackKitContext } from 'feedbackkit-react-native';

export function useEnsureUserId() {
  const { userId, setUserId, isInitialized } = useFeedbackKitContext();

  useEffect(() => {
    if (isInitialized && !userId) {
      setUserId(createStableAnonymousId());
    }
  }, [isInitialized, userId]);
}

setUserId writes through to AsyncStorage, so the identifier survives restarts and the user keeps their votes. The list itself renders fine without one — only the write paths fail, which is why this usually surfaces as "the board works but nothing can be voted on".

Everything fails with a 401. The errors are typed, so branch on them rather than string-matching messages:

import { AuthenticationError, NetworkError, ValidationError } from 'feedbackkit-react-native';

const { feedbacks, error } = useFeedbackList();

if (error instanceof AuthenticationError) {
  // 401 - key wrong, missing, or regenerated in project settings
} else if (error instanceof NetworkError) {
  // the request never reached the API
} else if (error instanceof ValidationError) {
  // 400 - the payload was rejected
}

An AuthenticationError almost always means the key is empty — a config module that resolved to undefined at build time — or belongs to a project whose key you regenerated. PaymentRequiredError is a different animal: a plan limit, not a broken key. The plan comparison lists what each tier includes.

Network errors on debug builds. A NetworkError means the request never completed. If it only happens on Android against a local server, look at your cleartext-traffic policy rather than the SDK. CORS is not involved here — that is a browser rule, and it belongs to the JavaScript SDK, not this one.

The list is empty and you are sure it should not be. Check filterByStatus first: if you filtered to Approved, freshly submitted items sit at pending and will not appear until you approve them. Also confirm you are pointed at the project you think you are — a debug key against a staging project is a convincing way to make real feedback invisible.

Next steps

The full prop and hook reference lives in the React Native SDK docs, and the OpenAPI reference covers the endpoints underneath if you need something the SDK does not wrap. The docs index has the equivalent guides for the other platforms — if your React Native app is one of several clients, the same project, board and votes back every one of them.

One last thing, which has nothing to do with code: seed the board with a few real items before you ship it, and reply to the first ten submissions. An empty board reads as abandoned, and a board nobody from the team ever answers teaches users that filing feedback is shouting into a well.

Keep reading

Collect feedback in your own app

FeedbackKit ships SDKs for Swift, Kotlin, React Native, Flutter and JavaScript, and syncs what your users ask for into the tracker your team already uses.