react native and recoil multiple re-renders of RecoilRoot - react-native

I have these components without any recoil's hooks.
const C = () => {
console.log('---->C')
return <Text>C</Text>
}
const B = () => {
console.log('--->B')
return <>
<Text>B</Text>
<C/>
</>
}
const A = () => {
console.log('-->A')
return <>
<Text>A</Text>
<B/>
</>
}
const App = () => {
console.log('->App')
return (
<RecoilRoot>
<A />
</RecoilRoot>
);
};
when i run the app in the console show up the spected logs:
LOG ->App
LOG -->A
LOG --->B
LOG ---->C
now im gonna use recoil hooks to mutate and access the atom state
import { atom, useSetRecoilState, useRecoilState, useRecoilValue, RecoilRoot } from "recoil";
const atomTest = atom({
key: "abcatomTest",
default: "A"
})
const C = () => {
console.log('---->C')
const [value, set] = useRecoilState(atomTest)
return <>
<Text>C</Text>
</>
}
const B = () => {
console.log('--->B')
const set = useSetRecoilState(atomTest)
return <>
<Text>B</Text>
<C/>
</>
}
const A = () => {
console.log('-->A')
const value = useRecoilValue(atomTest)
return <>
<Text>A</Text>
<B/>
</>
}
const App = () => {
console.log('->App')
return (
<RecoilRoot>
<A />
</RecoilRoot>
);
};
i dont even using the values and functions returned from useRecoilValue, useSetRecoilState, useRecoilState, if i use it, it works properly, BUT in the very first render the logs are:
LOG ->App
LOG -->A
LOG -->A
LOG ->App
LOG --->B
LOG ---->C
LOG ---->C
LOG --->B
LOG -->A
LOG ->App
LOG -->A
LOG --->B
LOG ---->C
why is recoil forcing the re-render of multiple components including root, im not mutating the state at all, and in the App component there is not dependency to any state neither!

First of all: React executing a function does not mean that the component actually re-renders.
React has a commit and a rendering phase. During the commit phase React goes through changes and calls the child components, checking if there is anything new to render. During the rendering phase React checks if there are components that actually have to re-render. If the outputs, hook states and props are identical there will be no re-render, even though React previously called your function component. This is why you see all the logs. You are not checking for re-renders with that, but for function executions.
Your App component actually has dependencies to state, since it renders the RecoilRoot component. When that component changes, React will enter the commit phase again and go through all children, to see if there are changes.
Since every component uses a hook that references the atomTest atom, Recoil has to subscribe to that atom for that components. So Recoil as well as React have to look for changes through the tree.
If you check with the Profiler of the React Developer Tools you'll see that there are no actual re-renders, since your components didn't change any output.

Related

Problem passing animatedPosition from BottomSheet to child to animate image

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.

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]);

React Hooks and useEffect – best practices and server issues

I am using React Native with functional components. componentDidMount() etc. are not available in functional components, instead I use Hooks. But Hooks don't act like lifecycle methods. I am wondering what the best practices are.
Assumed that we have a function like this one:
const ABCScreen = () => {
const [someHook, setSomeHook] = useState<any>()
useEffect(() => {
// some code inside this function which is called on every component update
}, [])
server.asyncCall().then(data => {
setSomeHook(data)
})
return (<View>
{someHook ? (<Text> `someHook` was assigned </Text>) : (<Text> `someHook` was not assigned, display some ActivityIndicator instead</Text>)}
</View>)
}
Where to place server.asyncCall()? Inside or outside of useEffect?
I think you have a misunderstanding here. The convention is that all the fetching data is going to be placed inside the componentDidMount lifecycle method. React useEffect hook can replace this easily by placing an empty array of dependencies, which means you can place that call inside the useEffect you already have.
Unlike you mention in your code comment, this hook won't be triggered on each component update. It will be only be triggered once the component is being mounted. So, you should be able to do it as follows:
const ABCScreen = () => {
const [someHook, setSomeHook] = useState<any>()
useEffect(() => {
server.asyncCall().then(setSomeHook)
}, [])//only triggered when component is mounted.
In the future, you might want to check the rules of the hooks.

React-Native - View config not found for name "Custom Tag"

I am using react-native-navigation v2 and every component needs to be registered into the navigation by calling registerComponent(). However, I found myself having 500 lines of code where I register every component of my app using the same registerComponent structure with the only difference of using different jsx tag for every component I register, like so:
import ItemsList from './src/components/ItemsList';
import { Navigation } from "react-native-navigation";
import { Provider } from "react-redux";
import reduxStore from "./src/store";
Navigation.registerComponent(
"app.ItemsList",
() => props => (
<Provider store={reduxStore}>
<ItemsList {...props} />
</Provider>
),
() => ItemsList
);
+++ 35 more components almost exactly just like this one
Now, in order to reduce that huge amount of identical code, I've decided to write an IIFE that maps through an array of objects(components) that look like:
[...,
{
name: "ItemsList",
component: ItemsList
},
...]
then calls registerComponent on every item and returns the JSX I need, like so:
(function componentsRegistration() {
return components.map(({ name, component }) => {
const Tag = name;
Navigation.registerComponent(
`app.${name}`,
() => props => (
<Provider store={reduxStore}>
<Tag {...props} />
</Provider>
),
() => component
);
});
})()
After this specific manipulation, my app doesn't render anymore. Instead it throws the "Invariant Violation: View config is not found for name ItemsList". I think I've made all of this respecting the React commandments (capital letter jsx tag, etc.), however can't get it to work. If anyone could, please help.
[SOLVED] I was getting an error because I was trying to pass a string with the component name instead of the component itself.
The right way to do it would be:
const Tag = component;
instead of:
const Tag = name;

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.