React Native doesn't re-render on DOM change - react-native

I had an array of components inside a ScrollView component. Somehow react native doesn't re-render when the array is modified.
Here's a demonstration of my problem:
const TestApp = () => {
const [arr, setArr] = useState([]);
function pushArr() {
setArr((arr) => {
arr.push(1);
return arr;
});
console.log('pushArr():', arr);
}
function flushArr() {
setArr([]);
console.log('flushArr():', arr);
}
useEffect(() => {
console.log('useEffect():' , arr);
})
return (
<>
<ScrollView style={{flex:1}}>
{arr.map((elem, i) => <Text key={i}>{elem}</Text>)}
</ScrollView>
<Button title="Push" onPress={pushArr}></Button>
<Button title="Flush" onPress={flushArr}></Button>
</>
)
}
The page remains blank, and no updates happen on button press.
I've logged out arr and these are my findings:
pushArr() and flushArr() works as expected
useEffect() gets triggered only on startup and after flushArr()
Can anyone explain this behavior, and what mistakes have I made?

If I remember correctly, you need to make a copy of the array whenever you want it to “react”. The new memory address will let react know it should update. In other words, you shouldn’t mutate the array.
You can use the spread operator to make a copy and then push an element to the end which you can then pass to useArr. Usually I see people just passing the new object inside your useArr function.
I also don’t see you passing anything to your useArr function.

Related

ReactNative UI freezing for a second before rendering a component with a fetch in useEffect()

TL;DR: My UI freezes for .5-1s when I try to render a component that does a API fetch within a useEffect().
I have ComponentX which is a component that fetches data from an API in a useEffect() via a redux dispatch. I'm using RTK to build my redux store.
function ComponentX() {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchListData()); // fetch list data is a redux thunk.
}, [dispatch]);
...
return <FlatList data={data} /> // pseudo code
}
as you can see the fetch will happen everytime the component is rendered.
Now I have ComponentX in App along with another component called ComponentY.
Here's a rudamentary implementation on how my app determines which component to show. Pretend each component has a button that executes the onClick
function App() {
const [componentToRender, setComponentToRender] = useState("x");
if (componentToRender === "x") {
return <ComponentX onClick={() => setComponentToRender("y")}/>
} else {
return <ComponentY onClick={() => setComponentToRender("x")}/>
}
}
Now the issue happens when I try to move from ComponentY to ComponentX. When I click the "back" button on ComponentY the UI will freeze for .5-1s then show ComponentX. Removing the dispatch(fetchListData()); from the useEffect fixes the issue but obviously I can't do that since I need the data from the API.
Another fascinating thing is that I tried wrapping the dispatch in an if statement assuming that it would prevent a data fetch thus resolving the "lag" when shouldReload is false. The UI still froze before rendering ComponentX.
useEffect(() => {
if (shouldReload) { // assume this is false
console.log("reloading");
dispatch(fetchListData());
}
}, [dispatch, shouldReload]);
Any idea what's going on here?
EDIT:
I've done a little more pruning of code trying to simplify things. What I found that removing redux from the equation fixes the issue. By simply doing below, the lag disappears. This leads me to believe it has something to do with Redux/RTK.
const [listData, setListData] = useState([]);
useEffect(() => {
getListData().then(setListData)
}, []);
Sometimes running the code after interactions/animations completed solves the issue.
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
dispatch(fetchListData());
});
}, [dispatch]);

How to create an rxjs Observable from TextInput (either onChange or onTextChange)

I want to create an observable from a change event that gets fired on a React Native TextInput component. TextInput comes with 2 change props that I'm aware of (onChangeText and onChange). From what I gather, you need to use onChange if you want access to the native event you need to use onChange.
I don't know much about the native event object. I am trying to create an rxjs observable using fromEvent.
First I created a ref in my functional component like this:
const sqftRef = useRef().current
Then I attached this ref to the TextInput component like this:
<TextInput
ref={sqftRef} // attach a ref
label='Sqft'
mode='flat'
textContentType='none'
autoCapitalize='none'
keyboardType='numeric'
autoCorrect={false}
value={String(formValues.sqft)}
dense
underlineColor={colors.colorOffWhite}
onChangeText={(text) => setText(text)}
onChange={e => {
// somehow create an observable from this event ???
}}
style={styles.inputStyles}
theme={inputTheme}
/>
I tried to create an Observable using fromEvent like this but it doesn't work. I get undefined is not an object (evaluating target.addEventListener):
fromEvent(sqftRef, 'onChange').subscribe(value => console.log(value))
I know my approach is all wrong. Hoping someone can point me in the correct direction.
I would emit events you need into a subject, then subscribe to the subject in other parts of your code.
Here's a simple React example that should get you started
function App() {
const textChange = new Subject<string>();
useEffect(() => {
// subscribe to
const subscription = textChange.asObservable().subscribe(console.log)
return () => subscription.unsubscribe()
}, [])
// Emit events with a subject
return <textarea onChange={(e) => {
textChange.next(e.target.value)
}}>
</textarea>
}
render(<App />, document.getElementById('root'));
Check out the example here: https://stackblitz.com/edit/react-ts-akoyfv
I think the problem is with assigning the current directly to the sqftRef. Try to define it without current, but use current when creating the Observable, like the following:
const sqftRef = useRef();
Then create the Observable within useEffect to make sure that the DOM is ready:
useEffect(() => {
fromEvent(sqftRef.current, 'onChange').subscribe((value) =>
console.log(value)
);
});
OK, I was able to figure it out with the help of Amer Yousuf and Alex Fallenstedt.
I did something similar to what Alex suggested, modifying his solution for React Native. One reason his solution wasn't working for me is that it is important to use the useRef hook to prevent the Observable from being re-created on each render. If the observable is recreated (on a re-render) and useEffect doesn't run again, then we won't have an active subscription to the newly (re-created) observable (useEffect never runs again). That's why my call to sqft$.next was originally only being called once (the first time until we re-render).
My solution looks like this:
let sqft$ = useRef(new BehaviorSubject(0)).current
useEffect(() => {
const sub = sqft$.subscribe({
next: (val) => {
// just testing stuff out here
updateForm('sqft', val)
updateForm('lot', val * 2)
}
})
// this is only relevant to my use case
if (activeReport) sqft$.next(activeReport.sqft)
return () => sub.unsubscribe()
}, [activeReport])
and of course I call this in onChangeText:
onChangeText={(text) => {
sqft$.next(text)
}}
So this is working right now. I still feel like there may be a better way using onChange(e => ...stuff). I will leave this question open for a little bit in case anyone can break down how to do this using nativeEvent or explain to me how I can access an event off the TextInput component.

what is the difference between passing an object with a variable and a lone variable to a react native component?

My lack of success in this problem may be due to a lack of proper terminology when Googling it but nonetheless I am completely stumped. I am passing an onPress function to a custom component in react native. When I pass it by itself as:
export const AddMorePlants = ( onPress ) => {
return (
<TouchableHighlight
onPress={onPress}>
.
.
.
}
I get a this2.props.onPress is not a function error but when I have the exact same code except with the onPress passed within curly braces:
export const AddMorePlants = ({ onPress }) => {
return (
<TouchableHighlight
onPress={onPress}>
.
.
.
}
It Works!
Why does the second one work and not the first?
Sorry for a kind of basic question I just have been really Googling and cant figure it out. Thanks in advance and I can provide any more info if needed.
A functional component in React only has one parameter. The props. You can read more about it here
So what your first attempt at passing the onPress function actually looks like is:
export const AddMorePlants = (props) => {
return (
<TouchableHighlight onPress={props}/>
);
}
When the TouchableOpacity tries to execute the method, it hits the is not a function error because props is an object.
When you do:
export const AddMorePlants = ({onPress}) => {
return (
<TouchableHighlight onPress={onPress}/>
);
}
what you are doing is something called destructuring assignment and it's equivalent of doing:
export const AddMorePlants = (props) => {
const {onPress} = props;
return (
<TouchableHighlight onPress={onPress}/>
);
}
By putting the brackets inside the parentheses you are just doing a shorthand version of this destructuring assignment that we have mentioned.
Here's another version that would also work:
export const AddMorePlants = (props) => {
return (
<TouchableHighlight onPress={props.onPress}/>
);
}
As you can see there are many ways to access an object's property.
The important part is to remember that the props object is the only parameter passed into a functional component.
I hope this helps you understand what's going on there.
In the first function you need to pass only one param i.e onPress to your component while in second you are destructuring assignment so you are doing something like onPress = this.props.onPress and passing an object of params.

Using FlatList#onViewableItemsChanged to call a Component function

I'm currently attempting to implement a form of LazyLoading using the FlatList component, which introduces a neat little feature called onViewableItemsChanged which gives you a list of all of the components that are no longer on the screen as well as items that are now on the screen.
This is a custom LazyLoad implementation and as such is more complicated than most LazyLoad open-sourced libraries that are available, which is why I'm working on my own implementation. I'm already looked into react-native-lazy-load and others.
Basically, I need to be able to call a function that's part of the component being rendered in the FlatList, I've tried creating a reference to the item rendered in the FlatList and calling it as such, but it doesn't seem to work.
For example:
<FlatList data={...}
renderItem={(item) => <Example ref={(ref) => this[`swiperRef_${item.key}`] = ref}}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
onViewableItemsChanged = ({viewableItems}) => {
viewableItems.forEach((item) => {
const { isViewable, key } = item;
if(isViewable && !this.cachedKeys.includes(key)) {
const ref = this[`swiperRef_${key}`];
if(!ref) return console.error('Ref not found');
ref.startLoading();
this.cachedKeys.push(key);
}
});
}
Now in the <Example /> component I would have a function called startLoading which should be called when a new visible item is brought onto the screen, however the ref never exists.
I was actually doing everything correctly, but I accidently forgot to deconstruct the parameter returned from the renderItem function, so (item) should have been ({ item })
That's all there was to it.

React Native mobx binding to FlatList not working

I have a RN (0.44.2) mobx (3.1.10) app which uses a FlatList. I'm basically following https://blog.callstack.io/write-react-native-apps-in-2017-style-with-mobx-e2dffc209fcb
When using my own store, opposed to the examples, I'm having to use toJS() in order to get the FlastList to render
// renders list
<FlatList
data={this.props.giphyStore.images.toJS()}
keyExtractor={(_, i) => i}
renderItem={({ item }) => <Text>found the data</Text>}
/>
// does not render list
<FlatList
data={this.props.giphyStore.images}
keyExtractor={(_, i) => i}
renderItem={({ item }) => <Text>did not find the data</Text>}
/>
I'm really struggling to figure out why toJS() might be needed in some cases and not others.
My store is setting the images observable like this
async getImageList(query: string) {
try {
const requestURL = `${constants.GIPHY_ENDPOINT}${query}`
const response = await axios.get(requestURL);
const imgs = response.data.data.map((item) => {
return { id: item.id, url: item.images.downsized.url }
})
this.images.replace(imgs)
} catch (e) {
}
}
As a follow up question, I'm not sure why I need to do the following this.images.replace(imgs) where as in the tutorial he simply did does this.tracks = response.data.tracks.items which triggers the observable just fine.
If anyone has suggestions, I would very much appreciate it.
This is because mobx's arrays are objects and the data in FlatList or in react native expects an array. You can read more about it in here and there.
Also..., slice returns a shallow copy; a new array with the same contents, while toJS also converts the values inside the array (but only if they are observables).
This question is kinda old, but it's also worth mentioning that MobX only tracks the render function by default, while FlatList accepts rendering callbacks and calls them. (eg renderItem={this.renderItem})
In order for items to update without the whole list refreshing, wrap the render callback's result with <Observer>.
See Understanding reactivity [Mobx docs]