Problem passing animatedPosition from BottomSheet to child to animate image - react-native

I cant seem to pass the onchange Y value from #gorhom/bottom-sheet. I can see the value when onchange in my App.js but I need to pass it another external component.
I am not sure if this involves using useDerivedValue or useEffect. I manage to pass the initial value from a state but even that is one behind.
I have an App.js that has the bottomSheet in it and another external .js file that loads images that are passed from the App.js, so everything is passing ok. Do I need to use useEffect to listen for changes?
As it stands I am using this in my App
const bottomSheetRef = useRef < BottomSheet > null;
// variables
const snapPoints = useMemo(() => [200, 538], []);
// callbacks
const handleSheetChanges = useCallback((index: number) => {
console.log("handleSheetChanges", index);
}, []);
const animatedPosition = useSharedValue(0);
const [currentPosition, setCurrentPosition] = React.useState(0);
useDerivedValue(() => {
console.log(
animatedPosition.value,
"here you will get value of every position"
);
}, [animatedPosition]);
useEffect(() => {
console.log(currentPosition);
}, [currentPosition]);
And how I am using it,
<BottomSheet
useRef={bottomSheetRef}
index={1}
snapPoints={snapPoints}
// onChange={handleSheetChanges}
animateOnMount={false}
animatedPosition={animatedPosition}
onChange={(index) => setCurrentPosition(index)}
>
This all works great but when I try to pass it, it does not update, I am using this code in my exertnal file,
export default function BottomSheetData(props) {
const {
show,
imga,
imgb,
imgc,
selectedItem,
imgWidthV,
imgHeightV,
animatedPosition,
} = props;
which gets all the relevant data just not the "live" changes?
I have tried everything but its now causing me a headache!!! I hope I have explained this issue properly and provided enough code. All I need it being pointed in the right direction or being told I am going around this in the totally wrong way.

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.

React Native doesn't re-render on DOM change

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.

Access variables in a `useEffect` without triggering the effect when they get updated

I need to access variables in a useEffect but only trigger the method when SOME of them get updated.
For example:
I want to call useEffect when data changes, but NOT when saveTimeout or saveMethod change.
In the same fashion, I want to call saveMethod when my component dismounts, but I can't because it needs to be in the dependency array, therefore calling it at every change of saveMethod.
function SavingComponent({data, apiInfo}){
[saveTimeout, setSaveTimeout] = useState(null);
const saveMethod = useCallBack(()=>{
clearTimeout(saveTimeout);
// api call to save the data using apiInfo
}, [data, saveTimeout, apiInfo])
// Start a timer to save the data 2 seconds after it is changed (not working)
useEffect(()=>{
clearTimeout(saveTimeout);
setSaveTimeout(setTimeout(saveMethod, 2000));
}, [data, saveTimeout, saveMethod]);
// Save immediately on dismount only (not working)
useEffect(()=>{
return ()=> { saveMethod(); }
},[saveMethod])
return (// some rendering)
}
This is an issue I am constantly running into with other cases and have to hack around. Usually using a custom usePrevious method. I would compare the previous value of the prop to the current and return immediately if the prop I am interested in didn't change.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
What is the proper way to only call useEffect when SOME dependencies get updated?
First thing that having variables and functions which are used in useEffect as dependencies is recommended and best practice. This is a very good article written by Dan Abramov which is deep dive in how useEffect works and why you need to follow that best practice.
Now back to your case.
I don't think you want the component re-render when you set saveTimeout, so instead use useState you can useRef for that saveTimeout = useRef(null) so you can remove saveTimeout from dependencies.
If you don't want saveMethod to be in dependencies you can move it inside useEffect so that you can remove it from dependencies too.
function SavingComponent({data, apiInfo}){
const saveTimeout = useRef(null);
// move saveMethod inside
useEffect(()=>{
const saveMethod = ()=>{
clearTimeout(saveTimeout.current);
// api call to save the data using apiInfo
}
// saveMethod will be called after 2 seconds
saveTimeout.current = setTimeout(saveMethod, 2000);
}, [data]);
// move saveMethod inside
useEffect(()=>{
const saveMethod = ()=>{
// api call to save the data using apiInfo
}
// saveMethod will be call when the component unmounted
// But make sure you are not update any state inside saveMethod
return saveMethod
},[])
return (// some rendering)
}

Pass variables via `this.props.navigation` multiple times

So, for begging, in react-native-navigation there's a possibility to pass some data via this.props.navigation.navigate().
Here's how you should pass the data :
this.props.navigation.navigate('RouteName', {/*Data to pass*/})
And so, moving to the problem
The case where this problem was encountered :
I have a list of items which I click on and I navigate to the next screen, the data of the pressed item being sent during the navigation process, and when I get to the next screen, the passed data is assigned to state, and I further operate with it. Here are the commands which I use for passing data:
Pass data
this.props.navigation.navigate('Screen2',{param1: value1, param2: value2})
Receive data
ComponentWillMount = () => {
const param1 = this.props.navigation.getParam('param1');
const param2 = this.props.navigation.getParam('param2');
this.setState({param1, param2)}
}
The Problem itself
My Problem is that if I go back to the first screen, and press on another item, then it's data isn't passed via this.props.navigation.navigate(), the data on the second screen remains unmodified from the first navigation process. How this problem can be resolved?
I think i figured it out,
I was able to replicate the issue using drawerNavigator and tabbed navigator in the react-navigation 3.0.5.
Basically they save the components even when you run navigation.goBack.
The screen isn't being mounted again so it doesnt call componentWillMount() and it doesn't check for data there.
there are 2 (edit 3) ways to fix this.
one is to turn off this performance enhancement
const MyApp = createDrawerNavigator(
{
Screen1: Screen1,
Screen2: Screen2
},
{
unmountInactiveRoutes: true
}
);
The second option and the more elegant one is to subscribe to navigation events
componentWillMount() {
console.log("mounting");
const willFocusSubscription = this.props.navigation.addListener(
"willFocus",
() => {
console.debug("willFocus");
const thing = this.props.navigation.getParam("thing");
const thing2 = this.props.navigation.getParam("thing2");
this.setState({thing, thing2});
}
);
}
Just dont forget to unsubscribe in componentWillUnmount
componentWillUnmount() {
willFocusSubscription.remove();
}
The third way is basically the same as the second but subscribing declaratively. This means no componentWillMount or WillUnmount.
First a callback to set the state appropriately
willFocus = ({action}) => {
console.debug("willFocus", action);
const thing = action.params["thing"];
const thing2 = action.params["thing2"];
this.setState({thing, thing2});
};
now in render add the component
render() {
console.log("data is:", this.state.thing);
return (
<View style={styles.container}>
<NavigationEvents
onWillFocus={this.willFocus}
/>
.... rest of render body
</View>
);
}
This doesn't display anything but it takes care of subscribing and unsubscribing.