I am fairly new to React Native and currently try to implement pull-to-refresh functionality in my app. Here is my Parent component snippet:
function MainScreenBoss({ navigation }) {
const [refreshing, setRefreshing] = useState(false);
//here I'm trying to refresh (example from docs)
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
wait(2000).then(setRefreshing(false));
}, [refreshing]);
return (
<ScrollView
contentContainerStyle={styles.containerScroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View style={styles.container}>
//other components here
<View style={styles.containerWidget}>
//need to refresh this component
<Widget style={styles.widget} />
</View>
</View>
</ScrollView>
);
}
I am trying to refresh Widget component as it has the call to API url in it with loading animation. Here is the snipped of Widget component:
function Widget() {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
//call to api returns data
useEffect(() => {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}, []);
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator
loader="blue"
visible={isLoading}
style={styles.animation}
/>
) : (
<>
<View style={styles.Contents}>
//need to refresh data is it used in here when loading is finished
</View>
</>
)}
</View>
);
}
I guess I need to force update the widget component or launch the loading function again somehow, but I do not quite understand what should I do.
Edit:
the api function looks like this:
export function getData(reqOptions, setLoading, setData) {
fetch(apiURL, reqOptions)
.then((response) => response.json())
.then((json) => setData(json.data))
.catch((error) => console.error(error))
.finally(() => setLoading(false));
}
If I understand well, in order to update your widget, you gotta re-do the fetch that you have inside your useEffect.
The useEffect you currently have only executes on mount of the component, as the dep. array is empty. From the looks of your parent component, the Widget does not get unmounted, therefore your useEffect is only called once.
What you have to do is to take your refreshing state and pass it to the Widget, so that it knows when it has to refetch the data.
Try something like this:
function MainScreenBoss({ navigation }) {
const [refreshing, setRefreshing] = useState(false);
//here I'm trying to refresh (example from docs)
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
wait(2000).then(setRefreshing(false));
}, [refreshing]);
return (
<ScrollView
contentContainerStyle={styles.containerScroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View style={styles.container}>
//other components here
<View style={styles.containerWidget}>
//need to refresh this component
<Widget style={styles.widget} refreshing={refreshing} /> // pass this prop
</View>
</View>
</ScrollView>
);
}
Widget:
function Widget(props) {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
//call to api returns data
useEffect(() => {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}, []);
useEffect(() => {
if (props.refreshing) {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}
}, [props.refreshing]); // on every refreshing `true` state
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator
loader="blue"
visible={isLoading}
style={styles.animation}
/>
) : (
<>
<View style={styles.Contents}>
//need to refresh data is it used in here when loading is finished
</View>
</>
)}
</View>
);
}
Edit:
You can either modify the existing useEffect to include the check of the data, or else create another one which runs only on mount, with another useEffect running on update state. I've added the second case.
Edit 2:
There is nothing wrong with that, actually that is better because the refreshing is handled at a correct time, when the call is finished and the data is fulfilled. With your current config the call may not be finished, but after 2 seconds you're setting refreshing to false, this may bring problems.
The beauty of hooks is that you can use as many of them as you want, as compared to the class methods of React before hooks introduction. So there is no problem with that, but you can actually change it around and have something like:
useEffect(() => {
if (!data || props.refreshing) {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}
}, [data, props.refreshing]);
If you may, one last thing: I would not pass state setters to your fetch fn, I would handle the state updates in the component and just make sure that the fn returns your data. It's just for separation of concerns causes.
Related
Im making a react-native app and I need to get some data (articles) from a GraphQL server and then, list the articles.
My problem is, when I run my app, the first time my HomeScreen component render, the state is empty and I cant see any of the articles because the response from the server takes some time to load.
I tried to use a condition where I check if the response has no errors and has finished loading, then I save the articles to the state son when I render my articleList, the state can have the articles, but it throws an error:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
All I need is to have an initial state in my application before I render my home component
My code:
const HomeScreen = () => {
const {loading, error, data} = useQuery(queryRepository.GET_ARTICLES);
const [articles, setArticles] = useState([]);
const filterByCategory = name => {
setArticles(CategoryService.filterByCategoryName(name, data.items));
};
if (loading) {
return <Spinner />;
} else if (error) {
return <Error>{error}</Error>;
} else {
setArticles(data.items);
}
return (
<View style={globalStyles.container}>
<View>
<View style={globalStyles.categoryMenuContainer}>
<CategoryMenu filterByCategory={filterByCategory} />
</View>
<View style={globalStyles.reviewsContainer}>
<ArticleList articles={articles} />
</View>
</View>
</View>
);
};
Try useEffect for set state.
const HomeScreen = () => {
const { loading, error, data } = useQuery(queryRepository.GET_ARTICLES);
const [articles, setArticles] = useState([]);
const filterByCategory = name => {
setArticles(CategoryService.filterByCategoryName(name, data.items));
};
useEffect(() => {
if (!loading && !error && data) {
setArticles(data.items);
}
}, [data, loading, error]);
if (error) {
return <Error>{error}</Error>;
}
if (loading) {
return <Spinner />;
}
return (
<View style={globalStyles.container}>
<View>
<View style={globalStyles.categoryMenuContainer}>
<CategoryMenu filterByCategory={filterByCategory} />
</View>
<View style={globalStyles.reviewsContainer}>
<ArticleList articles={articles} />
</View>
</View>
</View>
)
};
Everytime you call setArticles, a new render will happen in your component, hence why you should not use setters within the render function itself, you should use hook useEffect for that
const [number,setNum] = useState(0); I get this error when I want to add and change it(setNum(number+1)). My Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. What can i to solve this?
const App = ()=>{
const [text,setText] = useState('');
const [todo,setToDo] = useState([]);
const [number,setNum] = useState(0);
const renderToDoCard = ({item})=>{
setNum(number+1)
return(
<TouchableHighlight
onLongPress={() => handleLongPress(item)}>
<ToDoCard todo={item} number={number}/>
</TouchableHighlight>
)
}
const handleLongPress = item => {
setToDo(todo.filter(i => i !== item));
return Alert.alert('Silindi');
};
return(
<SafeAreaView style={styles.container}>
<StatusBar backgroundColor='#102027'/>
<View style={styles.head_container}>
<Text style={styles.title}>Yapılacaklar</Text>
<Text style={styles.title}>{todo.length}</Text>
</View>
<View style={styles.body_container}>
<FlatList data={todo} renderItem={renderToDoCard} />
</View>
<View style={styles.bottom_container}>
<ToDoInput todo={todo} setToDo={setToDo} text={text} setText={setText}/>
</View>
</SafeAreaView>
)
}
You've created an infinite update loop.
The problem is in how you're updating your number state inside renderToDoCard
const renderToDoCard = ({item}) => {
setNum(number + 1); // This is the problem, remove this line
return (
<TouchableHighlight onLongPress={() => handleLongPress(item)}>
<ToDoCard todo={item} number={number} />
</TouchableHighlight>
);
};
When renderToDoCard renders you update the state of your App component so it rerenders App which renders renderToDoCard which updates the state of your App component so it rerenders App which renders renderToDoCard...
This process repeats until the max update depth is reached.
Simply remove setNum(number + 1); and that problem is fixed.
It seems to me from your code that all you use your number state for is to keep track of the current item index so you can pass this to the ToDoCard component. The FlatList's renderItem also provides access to the current item index which you could pass to the number prop of ToDoCard
renderItem({ item, index, separators });
https://reactnative.dev/docs/flatlist#required-renderitem
So you could instead do something like this
const renderToDoCard = ({item, index}) => {
return (
<TouchableHighlight onLongPress={() => handleLongPress(item)}>
<ToDoCard todo={item} number={index} />
</TouchableHighlight>
);
};
Alternative you can add a key to each item in todo and use that instead of the index.
The image in the header constantly flickers when I type. May I ask how do I stop this flickering at the top right hand corner or accessoryRight? I am using this TopNavigation component from UI Kitten UI library. I don't think this is normal, it shouldn't happen at all. I must be doing something wrongly.
https://youtu.be/fQppQn-RzeE (How do I embed this? Editor, thank you in advance!)
The flickering happens in the title and the right side of the Navigation Header.
I made a separate component for the TopNavigation and then call it in respective screens.
Things I have tried:
Since the outcome of the Header relies on navigation props, I tried using useState and useEffect (with navProps as the dependency) to save the prop instead of reading directly from props, but to no avail
Directly adding the jsx into the TopNavigation's accessoryLeft/Right and title options
Any input is welcome, appreciate it!
TopNavigation:
const NavHeader = ({ navProps }) => {
const navigateBack = () => {
navProps.navigation.goBack();
};
const [type, setType] = React.useState('');
React.useEffect(() => {
setType(navProps.type);
}, [navProps]);
const BackIcon = props => <Icon {...props} name='arrow-back' />;
const BackAction = () => (
<TopNavigationAction icon={BackIcon} onPress={navigateBack} />
);
const renderBrand = () => (
<View style={styles.titleContainer}>
<Image source={require('../../assets/images/brand.png')} />
</View>
);
const renderLogo = () => (
<Image source={require('../../assets/images/logo.png')} />
);
return (
<TopNavigation
style={styles.topNav}
accessoryLeft={navProps.backNav && BackAction}
accessoryRight={
type === 'register' || type === 'profile' ? renderLogo : null
}
title={type === 'landing' || type === 'auth' ? renderBrand : null}
alignment='center'
/>
);
};
Import example:
<KeyboardAvoidingView
style={styles.kbContainer}
behavior={Platform.OS === 'ios' ? 'padding' : null}
>
<SafeAreaView style={styles.parentContainer}>
<NavHeader navProps={navProps} /> // Imported custom Header component here
<ScrollView>
{other content}
</ScrollView>
</SafeAreaView>
</KeyboardAvoidingView>
Perhaps requiring the images just one time and not on every render may help.
Try adding this at the top of the file (not inside a component function)
const brandImage = require('../../assets/images/brand.png');
const logoImage = require('../../assets/images/logo.png');
And then in your props instead of an inline require use the variables:
const renderBrand = () => (
<View style={styles.titleContainer}>
<Image source={brandImage} />
</View>
);
const renderLogo = () => (
<Image source={logoImage} />
);
Edit:
Since this didn't work, perhaps utilizing useMemo to memoize the components that show the images would work?
Something like
const renderBrand = useMemo(() => (
<View style={styles.titleContainer}>
<Image source={brandImage} />
</View>
),[]);
const renderLogo = useMemo(() => (
<Image source={logoImage} />
),[]);
I am trying to create a React Native e-commerce app where the featured products are shown, but then the user can view a list of categories via a sheet popping up from the bottom, which will load the products from said category.
I have managed to create such a bottom sheet using react-native-btr's BottomSheet. However, the function to show/hide the component (simply toggling a boolean in state) needs to be available to the component itself (to handle the back button and backdrop presses).
This is the component code:
const TestNav = (props, ref) => {
const [visible, setVisible] = useState(false);
const toggleVisible = () => {
setVisible(!visible);
};
useImperativeHandle(ref, () => toggleVisible());
return (
<BottomSheet
visible={visible}
//setting the visibility state of the bottom shee
onBackButtonPress={toggleVisible}
//Toggling the visibility state on the click of the back botton
onBackdropPress={toggleVisible}
//Toggling the visibility state on the clicking out side of the sheet
>
<View style={styles.bottomNavigationView}>
<View
style={{
flex: 1,
flexDirection: 'column',
}}
>
{DummyData.map((item) => {
return (
<Button
key={item.id}
title={item.name}
type="clear"
buttonStyle={styles.button}
onPress={() => console.log(item.name)}
/>
);
})}
</View>
</View>
</BottomSheet>
);
};
export default React.forwardRef(TestNav);
And here is the code for the screen where it's being used (it's called ChatScreen as I'm using it as a testing ground since I haven't implemented that feature yet)
import React, { useRef } from 'react';
import {SafeAreaView,StyleSheet,View,Text} from 'react-native';
import TestNav from '../components/TestNav';
import { Button } from 'react-native-elements';
const ChatScreen = () => {
const childRef = useRef(null);
const toggleBottomNavigationView = () => {
if (myRef.current) {
childRef.current.toggleVisible;
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<Text
style={{
fontSize: 20,
marginBottom: 20,
textAlign: 'center',
}}
>
Content goes here
</Text>
<Button
onPress={() => toggleBottomNavigationView()}
title="Show Bottom Sheet"
/>
<TestNav ref={childRef} />
</View>
</SafeAreaView>
);
};
export default ChatScreen;
However, this code has somehow triggered an infinite loop, as I get this message:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How do I go about fixing this?
I think the issue lies with how you define the imperative handle. Hook callbacks are called each time the component renders and so () => toggleVisible() is called each render and creates a render loop. It should be passed a callback that returns the imperative functions/values to be made available to callees.
const toggleVisible = () => {
setVisible(visible => !visible);
};
useImperativeHandle(ref, () => ({
toggleVisible,
}));
In ChatScreen you then need to invoke the function. I'll assume the myRef in your snippet was a typo since it's not declared in the component and the usage appears to be similar to the guard pattern.
const toggleBottomNavigationView = () => {
childRef.current && childRef.current.toggleVisible();
// or childRef.current?.toggleVisible();
};
I have a simple RN app that pretty much only has login so far. It uses Apollo GraphQL to call the backend and stores the user token and name in a React Context object. The context object has the user and a setUser function (and is instantiated in App.js). The login screen calls setUser from the UserContext object on a successful login.
This has been working fine but today I updated to React 16.3 and started getting the warning:
Cannot update a component from inside the function body of a different component
And lists the line where I am calling the setUser function if we received a succesful response.
The App.js uses useState to track the user and setUser function and puts that in a object to pass into the UserContext.Provider:
export default function App(props) {
const [user, setUser] = useState(null)
const contextVal = { user: user, setUser: setUser }
return (
<ApolloProvider client={client}>
<UserContext.Provider value={contextVal}>
<View style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<AppNavigator />
</View>
</UserContext.Provider>
</ApolloProvider>
);
}
And the LoginScreen retrieves user and setUser from the UserContext and when it gets data from the sigin mutation, tries to pass a simple user object to setUser:
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [signin, { data, error, loading }] = useMutation(SIGNIN_MUTATION)
const { user, setUser } = useContext(UserContext)
useEffect(() => {
if (user) {
props.navigation.navigate('Main')
}
}, [user])
if (loading) return (
<View style={styles.main}>
<Text style={styles.text}>Logging you in!</Text>
<ActivityIndicator size='large' />
</View>
)
if (data) { // ERROR IS HERE
setUser({
token: data.signin.token,
name: data.signin.user.name,
})
}
return (
<View style={styles.main}>
<Text style={styles.welcome}>Welcome to Bravauto!</Text>
<Text style={styles.error}>{error ? errorMessage(error) : ''}</Text>
<Text>Please login to continue:</Text>
<TextInput style={styles.input} onChangeText={(text) => setEmail(text)} placeholder="email" />
<TextInput style={styles.input} onChangeText={(text) => setPassword(text)} placeholder="password" secureTextEntry={true} />
<Button title="Login!" onPress={() => signin({ variables: { email: email, password: password } })} />
<Button title="No Account? Register Here" onPress={() => props.navigation.navigate('Registration')} />
</View>
)
I remember that to get this working I had to wrap the "already have a user" case (where it calls props.navigate) in a useEffect call, and I found other posts suggesting that wrapping this code in useEffect will fix the warning. However if I wrap this code in a useEffect hook like this:
useEffect(() => {
if (data) {
setUser({
token: data.signin.token,
name: data.signin.user.name,
})
}
})
I get an error instead of a warning:
Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
Any help would be much appreciated!
You will want to include the data field in the list of ones the useEffect is using:
useEffect(() => {
if (data) {
setUser({
token: data.signin.token,
name: data.signin.user.name,
})
}
}, [data])
Oops, forgot to post that I eventually found the answer to this. The solution was to use the callbacks provided by Apollo:
const [signin, { loading }] = useMutation(SIGNIN_MUTATION, {
onCompleted: (data) => {
setAuth(data.signin.token);
},
onError: (errorMsg) => {
setError(errorMessage(errorMsg));
},
errorPolicy: "none"
});
After changing to this approach everything works fine.