FlatList React Native перерисовывается при добавлении элемента в нормализованное и отсортированное хранилище Redux.
Когда я добавляю sortComparer в createEntityAdapter для сортировки списка по дате создания, любое добавление/удаление объектов приводит к обновлению всех элементов в моем списке. Если я удалю sortComparer, он будет работать нормально. Это происходит, хотя я запоминаю компоненты и функции.
Это мой редуктор, я использую setExpense для добавления/обновления расходов:
import {
createAction,
createAsyncThunk,
createEntityAdapter,
createSelector,
createSlice,
} from '@reduxjs/toolkit';
const baseName = 'expenses';
const adapter = createEntityAdapter({
selectId: expense => expense.id,
sortComparer: (a, b) => b.created_at.localeCompare(a.created_at),
});
const initialState = adapter.getInitialState({
...
});
export const expensesSlice = createSlice({
name: baseName,
initialState,
reducers: {
...
//ATUALIZAR/ADICIONAR DESPESA
setExpense(state, action) {
adapter.upsertOne(state, {
...action.payload,
sync: true,
sync_error: null,
});
},
...
},
extraReducers: ...
},
});
...
export const {
selectAll: selectAllExpenses,
selectIds: selectExpensesIds,
selectById: selectExpenseById,
selectEntities: selectExpensesEntities,
selectTotal: selectExpensesTotal,
} = adapter.getSelectors(state => state.expenses);
export default expensesSlice.reducer;
export const selectSingleExpensesIds = createSelector(
[selectExpensesIds, selectExpensesEntities],
(ids, entities) => {
return ids.filter(
id => !entities[id].request_id && !Boolean(entities[id].deleted),
);
},
);
Вот мой экран, на котором я отображаю список:
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { StyleSheet, View, VirtualizedList } from 'react-native';
import FocusRender from 'react-navigation-focus-render';
import { useDispatch, useSelector } from 'react-redux';
import ExpenseItem from '../../components/list-items/expense/ExpenseItem';
import {
selectSingleExpensesIds,
syncExpenses
} from '../../store/reducers/expensesSlice';
import { onChangeSelection } from '../../store/reducers/selectionSlice';
const ItemSeparator = () => {
return <View style={styles.separator} />;
};
export default function (props) {
const {
navigation
} = props;
const dispatch = useDispatch();
const expensesIds = useSelector(selectSingleExpensesIds);
const refreshing = useSelector(state => state.expenses.working);
const {type, selected} = useSelector(state => state.selection);
const isSelectionModeRef = useRef();
const selectedNum = useMemo(() => Object.keys(selected).length, [selected]);
useEffect(() => {
const bool = Boolean(type === 'SingleExpenses' && selectedNum > 0);
isSelectionModeRef.current = bool;
}, [type, Boolean(selectedNum > 0)]);
const toggleSelection = useCallback(expenseId => {
dispatch(
onChangeSelection({
type: 'SingleExpenses',
action: 'toggle',
payload: expenseId,
}),
);
}, []);
const handlePress = useCallback((expenseId, isRoute) => {
//Navegar para respectiva tela da despesa
if (isRoute) {
} else {
navigation.navigate('ExpenseView', {expenseId});
}
}, []);
const renderExpenseItem = useCallback(({item, index}) => {
return (
<ExpenseItem
key={`${item}-${index.toString()}`}
expenseId={item}
isSelectionModeRef={isSelectionModeRef}
selectionType="SingleExpenses"
onSelect={toggleSelection}
onPress={handlePress}
/>
);
}, []);
const keyExtractor = useCallback((item, index) => {
return item;
}, []);
const getItemLayout = useCallback(
(data, index) => ({length: 80 + 3, offset: (80 + 3) * index, index}),
[],
);
const getItemCount = useCallback(data => (data || []).length, []);
const getItem = useCallback((_data, index) => _data[index], []);
const handleRefresh = useCallback(() => {
dispatch(syncExpenses());
}, []);
return (
<FocusRender>
<View style={styles.container}>
<VirtualizedList
refreshing={refreshing}
onRefresh={handleRefresh}
style={styles.container}
data={expensesIds}
getItemCount={getItemCount}
getItem={getItem}
initialNumToRender={10}
renderItem={renderExpenseItem}
keyExtractor={keyExtractor}
contentContainerStyle={styles.listContentContainer}
ItemSeparatorComponent={<ItemSeparator />}
removeClippedSubviews
getItemLayout={getItemLayout}
/>
</View>
</FocusRender>
);
}
const styles = StyleSheet.create({
...
});
Вот элемент списка:
import React, { memo, useCallback } from 'react';
import { Pressable, View } from 'react-native';
import { TouchableRipple } from 'react-native-paper';
import { connect, useSelector } from 'react-redux';
import { useTheme } from '../../../context/ThemeContext';
import { selectExpenseById } from '../../../store/reducers/expensesSlice';
import { expenseItemData } from '../../../utils/ initialStates';
import Content from './components/Content';
import ErrorIndicator from './components/ErrorIndicator';
import LeftContent from './components/LeftContent';
import expenseItemStyles from './styles';
const ExpenseItem = ({
expenseId,
selected,
onSelect,
onPress,
isSelectionModeRef,
}) => {
const {
...
} = useSelector(
state => selectExpenseById(state, expenseId) || expenseItemData,
);
const {
isDarkTheme,
theme: {
colors: {
elevation: {level2},
},
},
} = useTheme();
const handleSelect = useCallback(
e => {
onSelect(expenseId);
},
[expenseId, onSelect],
);
const handleLongPress = useCallback(() => {
handleSelect();
}, [handleSelect]);
const handlePress = useCallback(
e => {
if (isSelectionModeRef?.current) {
onSelect(expenseId);
return;
}
onPress(expenseId, is_route);
},
[onPress, expenseId, is_route, onSelect],
);
console.log('re-render');
return (
<TouchableRipple
rippleColor={isDarkTheme ? 'rgba(255,255,255,.04)' : 'rgba(0,0,0,.06)'}
style={[expenseItemStyles.touchContainer]}
delayLongPress={200}
onLongPress={handleLongPress}
onPress={handlePress}>
<View
style={[
expenseItemStyles.container,
{backgroundColor: selected ? level2 : 'transparent'},
]}>
<Pressable
disabled={Boolean(sync)}
onPress={handleSelect}
onLongPress={() => console.log('Ver comprovante')}>
<LeftContent
syncError={Boolean(sync_error)}
sync={Boolean(sync)}
selected={selected}
isRoute={is_route}
receipt={receipts[0]}
/>
</Pressable>
<Content
notes={notes || ''}
amount={amount_converted || amount}
date={date}
currency={currency}
isRoute={is_route}
route={{
distance: distance || 0,
from: from || '',
to: to || '',
}}
expenseTypeId={type_id}
refundable={refundable}
syncError={Boolean(sync_error)}
/>
{Boolean(sync_error) && (
<ErrorIndicator errorMessage={sync_error || ''} />
)}
</View>
</TouchableRipple>
);
};
function mapStateToProps(state, ownProps) {
const {expenseId, selectionType} = ownProps;
const selected = Boolean(
state.selection.type &&
state.selection.type === selectionType &&
state.selection.selected[expenseId],
);
return {
selected,
expenseId,
...ownProps,
};
}
function expensePropsAreEqual(prevExpense, nextExpense) {
return prevExpense.selected === nextExpense.selected;
}
export default connect(mapStateToProps)(
memo(ExpenseItem, expensePropsAreEqual),
);
Я ожидал, что элементы будут нормально отображаться в соответствии с вашими изменениями, независимо от порядка данных.