Skip to content

Commit 5e9a6e0

Browse files
authored
FlashList v2 (#1617)
* Implement new RecyclerView prototype with enhanced features and React Native 0.78 support * Skip core swapping in tests * Add header offset to initial scroll * Add index to CellRendererComponent * Remove completed TODO comments * Fix memoization in samples * temporarily ignore fixture ts errors * Update yarn.lock * remove random in samples * build fix * Fix and add new e2e tests * pod update * Unit testing most of the code * Fix lint errors * Fix sticky overflow * Add docs 1.x and further edits to current docs * Fix chat flickers * Added TODO * Improve scrollToIndex alogrithm * version bump * Optimize scrollTo for non animated situations * v2.0.0-alpha.5 * Fix RTL horizontal lists * Add known issue for horizontal RTL * v2.0.0-alpha.6 * Fix unwanted mounts on data addition to top * Add note about horizontal lists * Update usage guide * Fix missing custom refresh control * v2.0.0-alpha.7 * Fix memo in docs * Reduce draw distance in chat sample * change load time log * Add new useMappingHelper hook for solving .map * Fix duplicate onStartReached calls * v2.0.0-alpha.8 * Fix flicker when switching to masonry from grid * Fix rendering on web and add a sample * Add more improvements for web * Fix e2e test * v2.0.0-alpha.9 * Add delay in e2e * bump flash-list in web fixture * Improve scrollTo accuracy even more * Improve velocity tracking which was missing on web and iOS * Fix bugs introduced by buffer improvements * Improve buffer with render time * Add wait to e2e * Fix e2e flakiness * v2.0.0-alpha.10 * Add an auto scroll script * Fix sticky crash * Improve twitter sample * Upgrade fixture to RN 0.79.1 * Update readme * Fix lint errors and cleanup * Code cleanup and add few more methods to FlashList's handler * Fix lint errors * Add measureLayout mocks * Update reanimated doc a bit * bug fixes * Add/Fix tests * Fix infinite render loop in Grid Layout * Improve sticky header compute * Link to v1 docs * Refactor recycling manager to be faster and more efficient * Reconfigure benchmarks * Export RecyclerView as AnimatedFlashList * Change displayName of RecyclerView * export recyclerview as default FlashList for tests * v2.0.0-alpha.11 * Improve sticky headers animated setup * Fix intermitttent animation skips in sticky headers * Sample improvements * Revert Animated.subtract as it breaks native driver * exp with mvcp in horizontal * Implement clearLayoutCacheOnUpdate API from v1 * v2.0.0-alpha.12 * Enable strict mode in few samples * Workaround for cases where useLayoutEffect fails to block paint * Fix missing itemType in average compute * Improve reconciliation (#1672) * Fix missing itemType in average compute * Improve reconciliation algo * Improve render stack update * Add comment * v2.0.0-alpha.13 * Remove completed TODOs * Prevent getItemType calls for negative indices * 2.0.0-alpha.14 * Fix documentation build * Improve docs * Doc improvements * Fix any in ViewToken * 2.0.0-rc.1
1 parent 242ffce commit 5e9a6e0

File tree

206 files changed

+37665
-14280
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

206 files changed

+37665
-14280
lines changed

.eslintrc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
"@shopify/jsx-no-complex-expressions": "off",
1919
"@shopify/react-prefer-private-members": "off",
2020
"eslint-comments/disable-enable-pair": "off",
21+
"@shopify/strict-component-boundaries": "off",
2122
"import/no-cycle": "off",
2223
"import/no-named-as-default": "off",
2324
"max-params": "off",
@@ -40,6 +41,12 @@ module.exports = {
4041
"@typescript-eslint/member-ordering": "off",
4142
"@typescript-eslint/consistent-indexed-object-style": "off",
4243
"jsx-a11y/no-autofocus": "off",
44+
"line-comment-position": "off",
45+
"react/no-unused-prop-types": "off",
46+
"no-negated-condition": "off",
47+
"no-nested-ternary": "off",
48+
"@babel/no-unused-expressions": "off",
49+
"@typescript-eslint/ban-ts-comment": "off",
4350
},
4451
overrides: [
4552
{

Gemfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ ruby ">= 2.6.10"
77
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1', '!= 1.15.2'
88
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
99
gem 'xcodeproj', '< 1.26.0'
10+
gem 'concurrent-ruby', '< 1.3.4'
11+
12+
# Ruby 3.4.0 has removed some libraries from the standard library.
13+
gem 'bigdecimal'
14+
gem 'logger'
15+
gem 'benchmark'
16+
gem 'mutex_m'
17+
18+
19+
source "https://rubygems.org"

Gemfile.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ GEM
2222
json (>= 1.5.1)
2323
atomos (0.1.3)
2424
base64 (0.2.0)
25+
benchmark (0.4.0)
2526
bigdecimal (3.1.8)
2627
claide (1.1.0)
2728
cocoapods (1.14.3)
@@ -79,6 +80,7 @@ GEM
7980
i18n (1.14.5)
8081
concurrent-ruby (~> 1.0)
8182
json (2.10.2)
83+
logger (1.7.0)
8284
minitest (5.23.1)
8385
molinillo (0.8.0)
8486
mutex_m (0.2.0)
@@ -111,7 +113,12 @@ PLATFORMS
111113

112114
DEPENDENCIES
113115
activesupport (>= 6.1.7.5, != 7.1.0)
116+
benchmark
117+
bigdecimal
114118
cocoapods (>= 1.13, != 1.15.2, != 1.15.1, != 1.15.0)
119+
concurrent-ruby (< 1.3.4)
120+
logger
121+
mutex_m
115122
xcodeproj (< 1.26.0)
116123

117124
RUBY VERSION

README.md

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
<a href="https://discord.gg/k2gzABTfav">Discord</a> •
66
<a href="https://shopify.github.io/flash-list/docs/">Getting started</a> •
77
<a href="https://shopify.github.io/flash-list/docs/usage">Usage</a> •
8-
<a href="https://shopify.github.io/flash-list/docs/performance-troubleshooting">Performance</a> •
98
<a href="https://shopify.github.io/flash-list/docs/fundamentals/performant-components">Writing performant components</a> •
109
<a href="https://shopify.github.io/flash-list/docs/known-issues">Known Issues</a>
1110
<br><br>
@@ -16,19 +15,33 @@ Swap from FlatList in seconds. Get instant performance.
1615

1716
</div>
1817

19-
## React Native's new architecture support
18+
# FlashList v2
2019

21-
FlashList v1 is compatible with React Native's new architecture however, we have a new version (v2) in alpha that fully leverages the new architecture and comes with more features. Click [here](https://github.com/Shopify/flash-list/tree/new-rlv-prototype?tab=readme-ov-file#flashlist-v2) to know more.
20+
FlashList v2 has been rebuilt from the ground up for RN's new architecture and delivers fast performance, higher precision, and better ease of use compared to v1. We've achieved all this while moving to a JS-only solution! One of the key advantages of FlashList v2 is that it doesn't require any estimates. It also introduces several new features compared to v1.
21+
22+
> ⚠️ **IMPORTANT:** FlashList v2.x has been designed to fully leverage the new architecture. **Old architecture will only be supported while FlashList v2 is in alpha/beta/rc and will be dropped once it's ready.** When run on old architecture, we just fall back to v1.x which doesn't have any of the new features.
23+
24+
### Is v2 production ready?
25+
26+
Yes, please use one of the release candidates if you want to ship to production `2.0.0-rc.x`. While we can make some changes in the final version, we expect release candidates to be quite stable. Use the alpha track if you want to test new changes quickly and don't mind occasional bugs.
27+
28+
> ⚠️ **IMPORTANT:** FlashList v2.x's alpha track moves quickly and can have some issues. Please report any issues or edge cases you run into. We're actively working on testing and optimizing v2. We also highly recommend using it with RN 0.78+ for optimal performance.
29+
30+
### Old architecture / FlashList v1
31+
32+
If you're running on old architecture or using FlashList v1.x, you can access the documentation specific to v1 here: [FlashList v1 Documentation](https://shopify.github.io/flash-list/docs/1.x/).
33+
34+
### Web support
35+
36+
FlashList v2 has web support. Most of the features should work but we're not actively testing it right now. If you run into an issue, please raise it on GitHub.
2237

2338
## Installation
2439

25-
Add the package to your project via `yarn add @shopify/flash-list` and run `pod install` in the `ios` directory.
40+
Add the package to your project via `yarn add @shopify/flash-list@rc` and run `pod install` in the `ios` directory.
2641

2742
## Usage
2843

29-
We recommend reading the detailed documentation for using `FlashList` [here](https://shopify.github.io/flash-list/docs/usage).
30-
31-
But if you are familiar with [FlatList](https://reactnative.dev/docs/flatlist), you already know how to use `FlashList`. You can try out `FlashList` by changing the component name and adding the `estimatedItemSize` prop or refer to the example below:
44+
But if you are familiar with [FlatList](https://reactnative.dev/docs/flatlist), you already know how to use `FlashList`. You can try out `FlashList` by changing the component name or refer to the example below:
3245

3346
```jsx
3447
import React from "react";
@@ -49,20 +62,99 @@ const MyList = () => {
4962
<FlashList
5063
data={DATA}
5164
renderItem={({ item }) => <Text>{item.title}</Text>}
52-
estimatedItemSize={200}
5365
/>
5466
);
5567
};
5668
```
5769

58-
To avoid common pitfalls, you can also follow these steps for migrating from `FlatList`, based on our own experiences:
70+
To avoid common pitfalls, you can also follow these steps for migrating from `FlatList`, based on our own experience:
5971

60-
1. Switch from `FlatList` to `FlashList` and render the list once. You should see a warning about missing `estimatedItemSize` and a suggestion. Set this value as the prop directly.
61-
2. **Important**: Scan your [`renderItem`](https://shopify.github.io/flash-list/docs/usage/#renderitem) hierarchy for explicit `key` prop definitions and remove them. If you’re doing a `.map()` use indices as keys.
72+
1. Simply from `FlatList` to `FlashList` and render the list.
73+
2. **Important**: Scan your [`renderItem`](https://shopify.github.io/flash-list/docs/usage/#renderitem) hierarchy for explicit `key` prop definitions and remove them. If you’re doing a `.map()` use our hook called [`useMappingHelper`](https://shopify.github.io/flash-list/docs/usage/#usemappinghelper).
6274
3. Check your [`renderItem`](https://shopify.github.io/flash-list/docs/usage/#renderitem) hierarchy for components that make use of `useState` and verify whether that state would need to be reset if a different item is passed to that component (see [Recycling](https://shopify.github.io/flash-list/docs/recycling))
6375
4. If your list has heterogenous views, pass their types to `FlashList` using [`getItemType`](https://shopify.github.io/flash-list/docs/usage/#getitemtype) prop to improve performance.
6476
5. Do not test performance with JS dev mode on. Make sure you’re in release mode. `FlashList` can appear slower while in dev mode due to a small render buffer.
77+
6. Memoizing props passed to FlashList is more important in v2. v1 was more selective about updating items, but this was often perceived as a bug by developers. We will not follow that approach and will instead allow developers to ensure that props are memoized. We will stop re-renders of children wherever it is obvious.
78+
79+
## Other things to know
80+
81+
- `keyExtractor` is important to prevent glitches due to item layout changes when going upwards. We highly recommend having a valid `keyExtractor` with v2.
82+
- `useLayoutState`: This is similar to `useState` but communicates the change in state to FlashList. It's useful if you want to resize a child component based on a local state. Item layout changes will still be detected using `onLayout` callback in the absence of `useLayoutState`, which might not look as smooth on a case-by-case basis.
83+
84+
```jsx
85+
import { useLayoutState } from "@shopify/flash-list";
86+
87+
const MyItem = ({ item }) => {
88+
const [isExpanded, setIsExpanded] = useLayoutState(false);
89+
const height = isExpanded ? 150 : 80;
90+
91+
return (
92+
<Pressable onPress={() => setIsExpanded(!isExpanded)}>
93+
<View style={{ height, padding: 16 }}>
94+
<Text>{item.title}</Text>
95+
</View>
96+
</Pressable>
97+
);
98+
};
99+
```
100+
101+
- `useRecyclingState`: Similar to `useState` but accepts a dependency array. On change of deps, the state gets reset without an additional `setState` call. Useful for maintaining local item state if really necessary. It also has the functionality of `useLayoutState` built in.
102+
103+
```jsx
104+
import { useRecyclingState } from "@shopify/flash-list";
105+
106+
const GridItem = ({ item }) => {
107+
const [isExpanded, setIsExpanded] = useRecyclingState(
108+
false,
109+
[item.id],
110+
() => {
111+
// runs on reset. Can be used to reset scroll positions of nested horizontal lists
112+
}
113+
);
114+
const height = isExpanded ? 100 : 50;
115+
116+
return (
117+
<Pressable onPress={() => setIsExpanded(!isExpanded)}>
118+
<View style={{ height, backgroundColor: item.color }}>
119+
<Text>{item.title}</Text>
120+
</View>
121+
</Pressable>
122+
);
123+
};
124+
```
125+
126+
- `useMappingHelper`: Returns a function that helps create optimal mapping keys for items when using `.map()` in your render methods. Using this ensures optimized recycling and performance for FlashList.
127+
128+
```jsx
129+
import { useMappingHelper } from "@shopify/flash-list";
130+
131+
const MyComponent = ({ items }) => {
132+
const { getMappingKey } = useMappingHelper();
133+
134+
return (
135+
<FlashList
136+
data={items}
137+
renderItem={({ item }) => <ItemComponent item={item} />}
138+
/>
139+
);
140+
};
141+
142+
// When mapping over items inside components:
143+
const NestedList = ({ items }) => {
144+
const { getMappingKey } = useMappingHelper();
145+
146+
return (
147+
<View>
148+
{items.map((item, index) => (
149+
<Text key={getMappingKey(item.id, index)}>{item.title}</Text>
150+
))}
151+
</View>
152+
);
153+
};
154+
```
155+
156+
- If you're nesting horizontal FlashLists in vertical lists, we highly recommend the vertical list to be FlashList too. We have optimizations to wait for child layout to complete which can improve load times.
65157

66158
## App / Playground
67159

68-
The [fixture](https://github.com/Shopify/flash-list/tree/main/fixture) is an example app showing how to use the library.
160+
The [fixture](./fixture/) is an example app showing how to use the library.

documentation/docs/fundamentals/performant-components.md

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
---
2-
id: performant-components
3-
title: Writing performant components
2+
id: performance
3+
title: Performance
44
---
55

6+
## Profiling
7+
8+
:::warning
9+
Before assessing your list's performance, make sure you are in release mode. On Android, you can disable JS dev mode inside the developer menu, whereas you need to run the release configuration on iOS.
10+
FlashList can appear to be slower than FlatList in dev mode. The primary reason is a much smaller and fixed [window size](https://reactnative.dev/docs/virtualizedlist#windowsize) equivalent. Click [here](https://reactnative.dev/docs/performance#running-in-development-mode-devtrue) to know more about why you shouldn't profile with dev mode on.
11+
:::
12+
13+
Memoizing props passed to FlashList is more important in v2. v1 was more selective about updating items, but this was often perceived as a bug by developers. We will not follow that approach and will instead allow developers to ensure that props are memoized. We will stop re-renders of children wherever it is obvious.
14+
15+
# Writing Performant Components
16+
617
While `FlashList` does its best to achieve high performance, it will still perform poorly if your item components are slow to render. In this post, let's dive deeper into how you can remedy this.
718

819
## Recycling
@@ -11,16 +22,12 @@ One important thing to understand is how `FlashList` works under the hood. When
1122

1223
## Optimizations
1324

14-
There's lots of optimizations that are applicable for _any_ React Native component and which might help render times of your item components as well. Usage of `useCallback`, `useMemo`, and `useRef` is advised - but don't use these blindly, always [measure the performance](/performance-troubleshooting) before and after making your changes.
25+
There's lots of optimizations that are applicable for _any_ React Native component and which might help render times of your item components as well. Usage of `useCallback`, `useMemo`, and `useRef` is advised - but don't use these blindly, always measure the performance before and after making your changes.
1526

1627
:::note
1728
Always profile performance in the release mode. `FlashList`'s performance between JS dev and release mode differs greatly.
1829
:::
1930

20-
### `estimatedItemSize`
21-
22-
Ensure [`estimatedItemSize`](/usage#estimateditemsize) is as close as possible to the real average value - see [here](/estimated-item-size#how-to-calculate) how to properly calculate the value for this prop.
23-
2431
### Remove `key` prop
2532

2633
:::warning
@@ -29,6 +36,10 @@ Using `key` prop inside your item and item's nested components will highly degra
2936

3037
Make sure your **item components and their nested components don't have a `key` prop**. Using this prop will lead to `FlashList` not being able to recycle views, losing all the benefits of using it over `FlatList`.
3138

39+
#### Why are keys harmful to FlashList?
40+
41+
FlashList's core performance advantage comes from **recycling** components instead of creating and destroying them however, when you add a `key` prop that changes between different data items, React treats the component as entirely different and forces a complete re-creation of the component tree.
42+
3243
For example, if we had a following item component:
3344

3445
```tsx
@@ -46,7 +57,7 @@ const MyItem = ({ item }) => {
4657
};
4758
```
4859

49-
Then the `key` prop should be removed from both `MyItem` and `MyNestedComponent`:
60+
Then the `key` prop should be removed from both `MyItem` and `MyNestedComponent`. It isn't needed and react can alredy take care of updating the components.
5061

5162
```tsx
5263
const MyNestedComponent = ({ item }) => {
@@ -63,38 +74,38 @@ const MyItem = ({ item }) => {
6374
};
6475
```
6576

66-
There might be cases where React forces you to use `key` prop, such as when using `map`. In such cirumstances, ensure that the `key` is not tied to the `item` prop in any way, so the keys don't change when recycling.
67-
68-
Let's imagine we want to display names of users:
77+
There might be cases where React forces you to use `key` prop, such as when using `map`. In such circumstances, **use `useMappingHelper`** to ensure optimal performance:
6978

7079
```tsx
71-
const MyItem = ({ item }: { item: any }) => {
72-
return (
73-
<>
74-
{item.users.map((user: any) => {
75-
<Text key={user.id}>{user.name}</Text>;
76-
})}
77-
</>
78-
);
79-
};
80-
```
80+
import { useMappingHelper } from "@shopify/flash-list";
8181

82-
If we wrote our item component like this, the `Text` component would need to be re-created. Instead, we can do the following:
83-
84-
```tsx
8582
const MyItem = ({ item }) => {
83+
const { getMappingKey } = useMappingHelper();
84+
8685
return (
8786
<>
88-
{item.users.map((user, index) => {
89-
/* eslint-disable-next-line react/no-array-index-key */
90-
<Text key={index}>{user.name}</Text>;
91-
})}
87+
{item.users.map((user, index) => (
88+
<Text key={getMappingKey(user.id, index)}>{user.name}</Text>
89+
))}
9290
</>
9391
);
9492
};
9593
```
9694

97-
Although using index as a `key` in `map` is not recommended by React, in this case since the data is derived from the list's data, the items will update correctly.
95+
The `useMappingHelper` hook intelligently provides the right key strategy:
96+
97+
- **When inside FlashList**: Uses stable keys that don't change during recycling
98+
- **When outside FlashList**: Uses the provided item key for proper React reconciliation
99+
100+
This approach ensures that:
101+
102+
- Components can be recycled properly within FlashList
103+
- React's reconciliation works correctly
104+
- Performance remains optimal
105+
106+
:::info
107+
`useMappingHelper` should be used whenever you need to map over arrays inside FlashList item components. It automatically handles the complexity of providing recycling-friendly keys.
108+
:::
98109

99110
### Difficult calculations
100111

@@ -182,8 +193,9 @@ const MyHeavyComponent = () => {
182193
return ...;
183194
};
184195

196+
const MemoizedMyHeavyComponent = memo(MyHeavyComponent);
197+
185198
const MyItem = ({ item }: { item: any }) => {
186-
const MemoizedMyHeavyComponent = memo(MyHeavyComponent);
187199
return (
188200
<>
189201
<MemoizedMyHeavyComponent />

documentation/docs/fundamentals/recycling.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ title: Recycling
44
slug: /recycling
55
---
66

7-
One important thing to understand is how `FlashList` works under the hood. When an item gets out of the viewport, instead of being destroyed, the component is re-rendered with a different `item` prop. For example, if you make use of `useState` in a reused component, you may see state values that were set for that component when it was associated with a different item in the list, and would then need to reset any previously set state when a new item is rendered:
7+
One important thing to understand is how `FlashList` works under the hood. When an item gets out of the viewport, instead of being destroyed, the component is re-rendered with a different `item` prop. For example, if you make use of `useState` in a reused component, you may see state values that were set for that component when it was associated with a different item in the list, and would then need to reset any previously set state when a new item is rendered. FlashList now comes with `useRecyclingState` hook that can reet the state automatically without an additional render.
88

99
```tsx
1010
const MyItem = ({ item }) => {
11-
const lastItemId = useRef(item.someId);
12-
const [liked, setLiked] = useState(item.liked);
13-
if (item.someId !== lastItemId.current) {
14-
lastItemId.current = item.someId;
15-
setLiked(item.liked);
16-
}
11+
// value of liked is reset if deps array changes. The hook also accepts a callback to reset anything else if required.
12+
const [liked, setLiked] = useRecyclingState(item.liked, [item.someId], () => {
13+
// callback
14+
});
1715

1816
return (
1917
<Pressable onPress={() => setLiked(true)}>
@@ -23,6 +21,4 @@ const MyItem = ({ item }) => {
2321
};
2422
```
2523

26-
This follows advice in the [React Hooks FAQ on implementing getDerivedStateFromProps](https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops). Ideally your component hierarchy returned from [renderItem](../fundamentals/usage.md#renderitem) should not make use of `useState` for best performance.
27-
2824
When optimizing your item component, try to ensure as few things as possible have to be re-rendered and recomputed when recycling.

0 commit comments

Comments
 (0)