In the following code, I expect OfferList to rerender when I add an offer item to the store. OfferList itself is not an observable, but the offer array is passed as a prop.
export const MerchantScreen: FC = observer(() => {
const { merchantStore } = useStores()
return (
<View>
<OfferList data={merchantStore.offers} />
<View>
<Button title={"New Offer"} onPress={() => merchantStore.addOffer()}/>
</View>
</View>
)
})
export const OfferList: FC<OfferListProps> = ({ data }: OfferListProps) => {
const renderItem = (offer: ListRenderItemInfo<any>) => {
return (
<Text>{offer.name}</Text>
)
}
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
)
}
I use Mobx State Tree. All merchantStore.addOffer() does for now is push another offer item into the array.
What I tried / findings:
When I read from the store in my MerchantScreen, e.g. by adding
<Text>{ merchantStore.offers.toString() }</Text>
, the OfferList will also update. I suspect that reading from the store directly in the parent component will force a rerender of the child component as well.
I stumbled upon some answers here that would indicate that a missing key attribute within the FlatList renderItems could be the issue. Tried using key={item.id} to no avail. Also, as you can see I use the keyExtractor prop of FlatList.
Another answers suggested introducing local state to the component like this:
export const OfferList: FC<OfferListProps> = ({ data }: OfferListProps) => {
const [offers, setOfferss] = useState()
useEffect(() => {
setOffers(data)
}, [data])
const renderItem = (offer: ListRenderItemInfo<any>) => {
return (
<Text>{offer.name}</Text>
)
}
return (
<FlatList
data={offers}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
)
}
This does not work and my gutfeeling is that this is not how it's done.
As you see my MerchantScreen parent component is an observable while my child component OfferList is not. From my expectation, the observer on my parent component should be enough. The parent component should already detect the change in the store and rerender. The child component in itself does not even use stores.
Overall, the problem at hand seems quite trivial so I guess I am just missing out on an important detail.
MobX only tracks data accessed for observer components if they are directly accessed by render, so if you want to react to each of the offers you need to access them somewhere. You sort of did when you tried merchantStore.offers.toString(), that's why it worked.
So first of all you need to make OfferList an observer.
But then you have FlatList which is native component and you can't make it an observer. What you can do is to access each offers item inside OfferList (just to subscribe for updates basically) like that data={offers.slice()} or even better with MobX helper method toJS data={toJS(offers)}
Depending on your use case you might also want to use <Observer> inside renderItem callback:
const renderItem = (offer: ListRenderItemInfo<any>) => {
return (
<Observer>{() => <Text>{offer.name}</Text>}</Observer>
)
}
Related
I'm working on a flatlist that has complex child that cause expensive rerender, I need to optimize that but I'm not able to stop the rerendering with useMemo, please help me to go through this.
Here my list code:
<FlatList
data={thePosts}
extraData={thePosts}
keyExtractor={(item, index) => index.toString()}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
updateCellsBatchingPeriod={30}
initialNumToRender={11}
windowSize={5}
refreshing={isRefreshing}
onRefresh={handleOnRefresh}
onEndReached={isLoading ? () => null : () => getPosts("more")}
onEndReachedThreshold={0.1}
renderItem={memoizedPost}
//renderItem={renderThePost}
ItemSeparatorComponent={renderThePostSep}
ListFooterComponent={renderThePostListFooter}
/>
here the renderPost:
const renderThePost = (props) => {
let post = props.item;
if (post[0].type == "share") {
return (
<TheSharedPost thePost={post} />
);
} else {
return <ThePost thePost={post} />;
}
};
I've tried to use memoization like this:
const memoizedPost = useMemo(() => renderThePost, []);
Now the problem is, the empty array as useMemo argument I think that only accept the first render but not working, I've tried to use [item.someProperty] but I'm not able to recognize item in the argument (item is not defined)
I've also used useCallback but still no luck, a lot o rerendering happen. Please help me to fix this. Tnz
you can use React.memo to avoid rendering of flatlist items
function TheSharedPost(props) {
/* render using props */
}
export default React.memo(TheSharedPost);
function ThePost(props) {
/* render using props */
}
export default React.memo(ThePost);
I have a screen, PDP, that screen contains a component, TopNews. I want TopNews onclick to redraw PDP (it passes in an Article ID which the PDP uses to retrieve the article). The diagram below shows the flow
The code I have to support this inside TopNews is;
<TouchableOpacity onPress={() => navigation.navigate('Pdp', {articleid: item.id})}>
The challenge is the TouchableOpacity event triggers, but the page doesn't refresh the PDP. I don't want to refresh only the PDP as I may include the TopNews component in other screens outside of the PDP, its just in this case its inside of the screen it needs to call.
There are more then one way to do it.
You can create a state for the article id and allow the TopNews to set this state:
const PDP = () => {
const [ id, setId ] = React.useState(0);
return (
<div>
<ArticleComponent articleId={id} />
<TopNews idSetter={setId} />
</div>
);
}
const ArticleComponent = ({ articleId }) => {
return /* something that renders the article by the id */ ;
}
const TopNews = ({ idSetter }) => {
return (
<TouchableOpacity
onPress={() => {
idSetter(item.id);
navigation.navigate('Pdp')
}}>
);
}
Another more elegant way, is to use react contexts. You can create a context provider in the PDP component and change the context data in any PDP's children component.
I found the answer, instead of using navigation.navigate, use navigation.push instead;
<TouchableOpacity onPress={() => navigation.push('Pdp', {articleid: item.id})}>
This works perfectly :)
I want to make a fetch request inside renderItem's componentDidMount method every time the list is refreshed, but the FlatList calls the lifecycle methods only once.
The list
<FlatList data={this.state.dataSource}
renderItem={({item}) => <ListItem imageHref={item.imageHref} />}
keyExtractor={(item, index) => index.toString()}
refreshing={this.state.refreshing}
onRefresh={/* Fetching data from JSON and updating dataSource[] */} />
Inside ListItem component:
render() {
return <Image source={this.state.imageSource} />
}
componentDidMount() {
fetch(this.props.imageHref)
.then(response => {
if(response.status !== 200)
this.setState({imageSource: require('../assets/default-image.png')
else
this.setState({imageSource: {uri: this.props.imageHref}});
}
});
}
I tried calling fetch inside render method but that didn't work either.
I basically want the imageSource to update every time the list is refreshed. Please help.
Because of that ListItem was not change. You have to fetch new icon in onRefresh and pass it into ListItem then all data will be changed when ListView will refresh... if you want to do it inside ListItem you need some interaction with that specific item, for example, some button and if user press it fetch and change icon
i didn't know when your list refreshed , but you can try this this.forceUpdate() you can learn more here force component to re render
I am trying to toggle ios Switch in react native. But the switch comes back to initial position as soon as I change it.
What I have:
class ABC extends Component {
constructor(props) {
super(props)
this.state = {
obj: []
}
}
fetch(){
// fetch something from remote server, set it to state object array
}
setStatus(id, value){
var temp = [...this.state.obj]
temp.map((t) => {
if (t.id == id) {
t.flag = value
}
})
this.setState({ obj: temp })
}
render() {
return (
<View>
<FlatList
data={this.state.obj}
renderItem={({ item }) =>
<View>
<Text>{item.name}</Text>
<Switch
onValueChange={(val) => this.setStatus(item.id, val)}
value={item.flag}
/>
</View>
}
keyExtractor={({ id }, index) => id.toString()}
/>
</View>
);
}
}
I logged the before and after value of obj state and they seem to update. Should the FlatList be rendered again (like a web page refresh) ? Or is there something I am missing ? Searched SO for answers, couldn't find my mistake.
Flatlist has a prop called extraData.
This prop tells Flatlist whether to re-render or not.
If data in extraData changes then flatlist re-renders based on new data provided in data prop.
So whenever you need to re-render flatlist just change something in extraData.
Best way is to pass state toextraData which is passed to Data.
So, just pass extraData={this.state.obj}.
there also other way called forceUpdate.
you can call this.forceUpdate().
but this is not recommended because this will render not only flatlist but entire component in which you are calling this.
I created a custom List component from the react-native flatList component. Now I want to use the scrollToIndex method of the flatlist component using ref so that I can scroll to top of the list by pressing a custom button. But the scrollToIndex method seems to be not available when I create a custom component. How to make this work?
You can pass it down through props
Your custom component "list" file:
const List = (props) => {
return (
<Flatlist
ref={(ref) => { this.listRef = ref; }}
initialScrollIndex={this.props.initialScrollIndex}
/>
);
};
In use:
scrollToIndex = () => {
this.listRef.scrollToIndex({animated: true, index: whateverIndex});
}
<List
initialScrollIndex={whateverIndex}
/>