Articles · How toUpdated October 2026

An activity feed is three jobs, and the system owns the third one.

Adding an activity feed to an app means solving three problems that have nothing to do with each other. Drawing a long list whose contents keep changing is yours, and React Native documents it properly. Putting a number on the app icon is yours too, and Expo gives you two calls for it. Getting new rows onto a phone while the app is shut is not yours at all, and that is the part worth reading before you design anything. If you have not built the app this feed goes into yet, start at make an app.

Every number and every quoted sentence below comes from React Native's, Expo's or Apple's own reference pages, read on 23 October 2026, and the dates those pages carry are printed next to the figures. The short answer to the question people usually arrive with: there is no documented limit on how often a feed may poll, but there is a 15 minute floor on scheduled background work and a request from Apple to send no more than two or three background pushes an hour.

Start here

The short version

The list is a solved problem. The timing is not yours to solve.

A feed is a list that changes while somebody is looking at it, and that single property is what separates it from every other screen in an app. React Native's list components are built for long data, and they are also built to skip work, so a feed hits the same bug again and again: you change something about an item, the array that holds it is the same array, and the list quite correctly decides it has nothing to do. The fix has a name in the documentation and takes one prop.

The harder half is the clock. People want a feed that is current, and current means something arrived while the app was closed. Both platforms answer that with a background budget they control, numbers that are floors rather than schedules, and an explicit refusal to guarantee delivery. Plan for a feed that is honest about when it last looked, rather than one that pretends to be live, and nothing on this page will surprise you later.

Why does a feed stop updating when the data changes?

Because the list is built to skip renders, and it says so. Both the FlatList and the VirtualizedList references carry the same caveat: the component implements PureComponent, which means it will not re-render if props are shallow-equal, and everything your renderItem function depends on has to be passed as a prop that is not identical after an update. That includes the data prop itself. If you reach into your array, flip an item's read flag and call setState with the same array, the array is shallow-equal to the one before it and nothing happens on screen.

React Native names the prop that solves this. extraData is described as a marker property for telling the list to re-render, with the instruction that if any of your renderItem, header or footer functions depend on anything outside the data prop, put it there and treat it immutably. The FlatList page adds that extraData is essential whenever renderItem has a stable reference, such as when it is memoized or defined outside the component. That is the same page telling you to memoize renderItem, so the two pieces of advice arrive together and the second one is the tax on the first.

The second documented trap belongs to feeds specifically, because feeds grow at the top. keyExtractor is described as extracting a unique key for each item, used for caching and as the react key to track item re-ordering, and the default extractor checks item.key, then item.id, and then falls back to using the index. Index keys are fine for a list that never moves. In a feed that puts three new rows at the front, every index now points at different content, and the list has been told, by you, that row 4 is still row 4. Rows reuse the wrong content, animations fire on the wrong items, and the whole thing looks like a rendering bug rather than a key problem. Use the id your server already sends.

One more caveat is easy to read past and expensive to meet in production. Internal state is not preserved when content scrolls out of the render window, and the documentation tells you to make sure all your data is captured in the item data or in an external store. A comment box half typed, a row expanded to show detail, a toggle somebody just flipped: all of it goes when the row leaves the window, because the row was unmounted. On a feed that is a long scroll by definition, that happens constantly.

React Native, Optimizing FlatList Configuration, last updated 12 August 2026, read 23 October 2026

Which list settings are worth touching, and which are not?

React Native keeps the tuning knobs on one page and gives each a default and a cost. windowSize is 21, explained as a measurement where 1 equals your viewport height, so the default mounts roughly ten screens above and ten below what you can see. maxToRenderPerBatch is 10, the number of rows added on each scroll chunk. updateCellsBatchingPeriod is 50 milliseconds, the delay between those batches. initialNumToRender is 10. Every one of them trades the same two things against each other: blank space while scrolling against memory and time on the JavaScript thread.

Two of these deserve a second look on a feed. initialNumToRender at 10 may be fewer rows than a tall phone shows, and the page warns that a low value causes blank areas if it is too small to cover the viewport on first render, so set it to what actually fills your largest supported screen. removeClippedSubviews is the one to be careful with: it defaults to true on Android and false everywhere else, which means your two platforms do not start from the same configuration. React Native's own note says the implementation can have bugs such as missing content, mainly observed on iOS, and that it does not save significant memory anyway, because the views are detached rather than deallocated.

getItemLayout is the biggest win on the page and the one a feed usually cannot have. It removes the need for the list to manage async layout calculations, but only if every row is the same height. A feed where one row is a line of text and the next carries a photo does not qualify. The documentation's advice when sizes are dynamic and performance matters is to go back to the design and ask whether rows can be made uniform, which is an unusually honest thing for an API reference to say and worth taking seriously before you start measuring rows by hand.

The rest of the page is about the row component rather than the list. Keep rows simple, avoid heavy images in favour of thumbnails, wrap the row in memo with a comparison function so it re-renders only on the props that changed, and move renderItem out of the returned JSX and into a useCallback so it is not recreated on every render. None of that is exotic. It is the difference between a feed that scrolls and one that stutters on the cheapest phone your customers own.

Check yours

Nine feed symptoms and what the docs say causes them

Tick whatever your feed is doing. Every cause below is a sentence or a default value from React Native's or Expo's own reference pages, read on 23 October 2026. None of it is guesswork about your code.

Nothing ticked. If the feed is behaving, the two settings worth checking anyway are keyExtractor, because an index key only misbehaves once the list starts receiving new items at the top, and the badge, because it looks correct until the first time somebody closes the app.

React Native, props, defaults and the list item advice, read 23 October 2026

How do you put the number of new items on the app icon?

With two calls from expo-notifications, and one idea that catches people out. setBadgeCountAsync takes a number and sets the badge on the app icon, where a value of 0 clears it. getBadgeCountAsync reads the current number back, and Expo notes that a 0 result means the badge is not displayed. The idea behind both: the badge is a number you set. The system does not know how many rows in your feed are unread and will never work it out, so every badge you see on any phone is a figure some code wrote at some moment.

Each platform attaches a condition. On iOS, setBadgeCountAsync needs you to have asked for allowBadge through requestPermissionsAsync first, and Expo says that without it the call will automatically return false. The default permission request asks for alerts, badge count and sounds together, so you get it by default, but the moment you narrow the request you can lose the badge without any error worth noticing. On Android, Expo's note is blunter: not all launchers support application badges, and where the launcher does not, the setter resolves to false and the getter always resolves to 0. The badge is an extra on Android, not a surface you can rely on.

Which leaves the question of when the number gets written, because your code is not running while the app is closed. Expo documents two ways in. The notification content type carries a badge field, described as the application badge number associated with the notification, so your server can send the correct count alongside the alert and nothing of yours has to execute. And the notification handler takes shouldSetBadge, which decides whether an arriving notification updates the badge, marked as iOS only. That handler has to answer within 3 seconds or the notification is presented anyway, which is a good reason to keep the decision in it trivial.

Clearing it is the half people forget. Call setBadgeCountAsync(0) when the feed is opened and read, not when the app launches, or you will clear a count somebody never saw. A counter on a tab bar is the same mechanic, which is why add shopping cart runs into the same decision about where the authoritative count lives.

Expo, Notifications reference, manage application badge icon, read 23 October 2026

Five ways a new row reaches the phone

The middle column is the one that decides your design. Everything that works while the app is closed comes with a timing promise you did not write and cannot tighten, and everything that does not comes with no limits at all. Pick the row you can live with before you pick the library.

How the row arrivesWorks with the app closedWhat the documentation promises about timing
You refresh when the app comes to the foregroundNoImmediate, because you are the one asking. None of the three reference pages cited on this page puts a ceiling on how often a foreground app may call your own server.
An open socket while someone is looking at the feedNoAs fast as your server sends. Again no documented ceiling on those pages, because the app is awake and in front of a person who asked for it.
A scheduled background taskYesMinimum interval 15 minutes, default once every 12 hours, and the system treats your number as a minimum delay rather than a schedule.
A background push carrying content-availableYesNo delivery guarantee. Apple says the system may throttle these if the total becomes excessive, and asks you not to send more than two or three per hour.
A visible push carrying a badge numberYesArrives when the notification arrives and sets the count without running any of your code, which is why it is the cheapest way to keep a badge honest.

Is there a documented limit on how often a feed may poll?

Not in the form people look for. There is no published rule saying a feed may refresh once a minute and not more, and while the app is in the foreground nothing in these references constrains you at all. The limits live entirely in the background, and they are not a rate cap. They are a floor you cannot go below and a schedule you do not control.

Take the floor first. Expo's background task reference gives minimumInterval a default of once every 12 hours and a minimum of 15 minutes, and then explains what that number really is: the system controls execution and treats your value as a minimum delay, tasks will not run exactly on schedule, and on iOS short intervals are often ignored because the system prefers particular windows such as overnight. On Android the same 15 minute minimum comes from WorkManager. On iOS the decision belongs to BGTaskScheduler, which weighs battery level, network availability and how the person uses the app. A 15 minute interval is a request, and the honest reading is that you have asked for the task to be considered no sooner than that.

Now the ceiling, which is the closest thing to a published number. Apple's page on pushing background updates to your app says the system treats background notifications as low priority, does not guarantee their delivery, and may throttle them if the total becomes excessive. It then gives a figure: the number allowed depends on current conditions, but do not try to send more than two or three per hour. Expo's notifications reference repeats that figure and adds the Android case, noting the OS may decide not to deliver a notification when the device is in Doze mode. Three more behaviours from Apple's page shape what a feed can promise. The app gets 30 seconds once woken. When a newer background notification arrives the system discards the older one and holds only the newest. And if the person launches the app, the held notification is delivered immediately.

Two practical consequences follow. The first is that a feed cannot be live through background work, so show when it last refreshed and refresh on foreground, where you have no limits. The second is about testing: Expo states that the Background Tasks API is unavailable on iOS simulators and works only on a physical device, so none of this path can be checked where most people check their work. The same delivery route carries every other alert your app sends, which is why add referral program meets the same two or three per hour ceiling from a different direction.

Expo, Notifications reference, running JavaScript in response to incoming notifications, read 23 October 2026

Where this lands if a builder generated your app

Newly is an AI app builder, so describe the feed in plain English and it writes a real React Native and Expo project you own, with two-way GitHub sync so the code is somewhere you can read it. That matters more than usual here, because every fix on this page is one line in a file: a keyExtractor that points at an id, an extraData prop, an initialNumToRender that matches your tallest screen, a setBadgeCountAsync(0) in the right place. A builder you cannot read the output of leaves you guessing at which of the nine symptoms above you have. Newly costs $25 a month, with no free tier.

It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK, which is the only honest way to test the background half of a feed, since the Background Tasks API does not run on a simulator. What it does not do is press submit on the App Store for you. Apple requires the owner of the content to submit, so the store account and that final step stay with you, and the mechanics are in publish to app store.

One recommendation to close on, and it is a design one rather than a technical one. Decide what the feed is allowed to claim. A header that reads "updated 2 minutes ago" and refreshes when someone opens the app is truthful, cheap and never wrong. A feed that implies it is live, on a platform that has told you in writing it will not guarantee delivery and prefers to run your task overnight, is a promise you have no way to keep.

Questions people ask about adding an activity feed

FlatList, and the VirtualizedList it wraps. The relevant part for a feed is not the scrolling, it is the re-render rule: both references say the list implements PureComponent and will not re-render when props are shallow-equal. The documented answer is the extraData prop, which the FlatList reference calls a marker property for telling the list to re-render, with the instruction to put anything your renderItem depends on there and treat it immutably. A second caveat matters just as much for a feed: internal state is not preserved when content scrolls out of the render window, so a row that holds its own state loses it. Read on 23 October 2026. The FlatList reference carries a last updated date of 19 October 2026 and the VirtualizedList reference 12 August 2026.

Write down what the feed is allowed to promise.

Three lines are enough of a brief: what counts as an event, who is allowed to see it, and how stale the oldest row on screen is allowed to be. The third line is the one that decides whether you need background delivery at all, and most feeds turn out not to.

Start building