I have a useEffect() which should be called once but continuously it is calling the getStories method.
const [pageNo, setPageNo] = useState(1);
const [recordsPerPage, setrecordsPerPage] = useState(5);
const { jwt } = useSelector((state: StateParams) => state.account);
useEffect(() => {
if (jwt) {
dispatch(
getStories({ token: jwt, pageNo: pageNo, recordsPerPage: recordsPerPage })
);
}
}, []);
UPDATED: I feel flatlist is causing
<FlatList
style={{
marginTop: 14,
alignSelf: "stretch"
}}
data={Items}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
const renderItem = useCallback(({ item } : {id: string}) => {
console.log({item})
const Section = Components[item.id]
return (<Section {...props}/>)
}, [Items])
Related
i'm using the tab view react native library to build a user account screen. my request is simple, how can i update the tab view content after an api call that fetches the user data?
function UserStoreScreen({ navigation, route }) {
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(0);
const [userStore, setUserStore] = React.useState({});
const [routes] = React.useState([
{ key: "first", title: "Dressing" },
{ key: "second", title: "À propos" },
]);
const user = route.params;
// renders undefined
const FirstRoute = () => (
<>
<View style={styles.userContainer}>
<ListItem
image={`${IMAGES_BASE_URL}${userStore.photo}`}
title={`${userStore.username}`}
subTitle={`${userStore.store.length} Articles`}
/>
</View>
</>
);
const SecondRoute = () => (
<>
<View style={{ flex: 1, backgroundColor: "#ff4081" }} />
</>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
const getUser = async () => {
await axiosApi
.post("/getUserProducts", { user_id: user.user_id })
.then((response) => {
// didn't work since set state is async
setUserStore(response.data);
})
.catch((err) => {
console.log(err);
});
};
// Get store products
useEffect(() => {
getUser();
}, []);
return (
<Screen style={styles.screen}>
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
}
is there a way to make the content of the tab view updated after i receive the data from the api call?
Yes, there is a way to forcefully re-mount a component. To do that, we can use key props like this:
return (
<Screen style={styles.screen}>
<TabView
key={JSON.stringify(userStore)}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
How does key props work? Every time a component is re-rendering, it will check whether the key value is the same or not. If it's not the same, then force a component to re-render.
In this case we will always check if userStore value has changed or not.
How do I invalidate data after refreshing the page? It doesn't seem to invalidate while it is supposed to. It still displays the old data even though something changed on the server-side.
I have this same problem when I use useMutation when posting data to the backend, the UI doesn't update even after using the QueryClient.
Below is my code:
const IncomeManager: React.FC<any> = (props) => {
const queryClient = new QueryClient();
const {isLoading, isError, isFetching, data}: QueryObserverResult = useQuery('typeIncomes', () => typesApi.getAllTypes());
const [refresh, setRefresh] = useState<boolean>(false);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
//#ts-ignore
const handleClose = (): void => {
setIsModalVisible(false);
}
const refreshContent = async () => {
await queryClient.invalidateQueries('typeIncomes');
console.log("Content has been refreshed!!!");
}
return (
<View style={style.container}>
<View>
<AppText style={style.title}>{data ? data.length : 0} income types available</AppText>
</View>
<FixedButton
title={"plus"}
onPress={() => props.navigation.navigate(navConstants.ADDTYPE, {type: "incomes"})}
/>
{
isLoading || isFetching ? <PageActivityIndicator visible={isLoading || isFetching}/> :
<FlatList style={{width: "100%"}}
data={data}
renderItem={
({item}) => <CategoryItem
id={item.type_id}
title={item.title}
subTitle={item.description}
onLongPress={() => console.log("Very long press!")}
onPress={() => props.navigation.navigate(navConstants.EDITTYPE,
{
item: {
id: item.type_id,
title: item.title,
description: item.description
}
})
}
/>
}
keyExtractor={item => item.type_id}
refreshing={refresh}
onRefresh={async () => refreshContent()}
/>
}
</View>
);
}
export default IncomeManager;
const style = StyleSheet.create({
container: {
flex: 1,
width: "100%",
backgroundColor: constants.COLORS.secondary,
alignItems: "center"
},
title: {
color: constant.COLORS.lightGray,
paddingVertical: 10,
fontSize: 17,
marginBottom: 0
},
});
You are creating a new QueryClient every time your component renders by doing:
const queryClient = new QueryClient()
The queryClient holds your cache, which holds your data. There should be only one (like a redux store) - the one you create initially and then pass to the QueryClientProvider. To retrieve this Singleton instance, you can do:
const queryClient = useQueryClient()
it will give you the instance via React context. Invalidation on that queryClient should work. This is also how everything in the docs and all the example are set up.
I have a fairly basic FlatList component implemented using hooks. The list simply loads data from a random user API and lazy loads additional data via infinite scroll. The only visual issue I'm experiencing is that when I merge the new data with the current, the new data being appended flickers very briefly before fully rendering. Not sure what could be causing this.
Expo Snack
const useRestApi = (url) => {
const [ data, setPeople ] = useState([]);
const [ page, setPage ] = useState(1);
const [ results, setResults ] = useState(20);
const [ loading, setLoading ] = useState(false);
useEffect(() => {
const fetchPeople = async () => {
setLoading(true);
const response = await fetch(`${url}&page=${page}&results=${results}`);
const json = await response.json();
if(page !== 1)
setPeople([...data, ...json.results]);
else
setPeople(json.results);
setLoading(false);
}
fetchPeople();
}, [page]);
return [{data, loading, page}, setPage, setResults];
}
const App: () => React$Node = () => {
const [{ data: people, loading, page }, setPage, setResults] = useRestApi(`https://randomuser.me/api?&seed=ieee`);
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<FlatList
data={people}
onEndReachedThreshold={0.2}
keyExtractor={(item, index) => index.toString()}
renderItem={({item, index}) => (
<View key={index} style={styles.listItem}>
<Text style={styles.listItemHeader}>{item.name.first} {item.name.last}</Text>
<Text style={styles.listItemSubHeader}>{item.location.country}</Text>
<Text style={styles.listItemBody}>{item.location.street.number} {item.location.street.name}</Text>
<Text style={styles.listItemBody}>{item.location.city} {item.location.state} {item.location.postcode}</Text>
</View>
)}
refreshing={loading}
onRefresh={() => {setResults(20); setPage(1);}}
onEndReached={() => {setResults(5); setPage(page + 1);}}
ItemSeparatorComponent={() => ItemSeparatorComponent}
ListFooterComponent={() => loading ? ListFooterComponent : null}
/>
</SafeAreaView>
</>
);
};
I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.
const [page, setPage] = useState(1);
This is my useEffect Hook:
useEffect(() => {
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
setProducts([...products, ...results.data]);
setIsLoading(false);
};
loadProducts();
}, [page]);
Offset is ${page}, limit is &perPage=5 (hardcoded to 5)
Flatlist:
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderGridItem}
onEndReached={loadMore}
onEndThreshold={0.3}
/>;
LoadMore:
const loadMore = () => {
setPage(page + 1);
};
In theory, this should work, shouldn't it?
Description
I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)
The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.
In this example we use reqres.in to simulate a fetch to some data.
The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.
Expo snack
A snack of the example can be found here: https://snack.expo.io/BydyF9yRH
Make sure to change platform to iOS or Android in the snack (Web will not work)
Code
import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')
import {
StyleSheet,
Text,
View,
SafeAreaView,
SectionList,
Button,
RefreshControl
} from 'react-native';
function Item(item) {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title.first_name}</Text>
</View>
);
}
export default function testSectionList({ navigation }) {
const [data, setData] = React.useState()
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false);
const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);
const dataIndex = React.useRef(0);
const totalHits = React.useRef(42); // In real example: Update this with first result from api
const fetchData = async (reset: boolean) => {
if (reset === true) dataIndex.current = 0;
// Make sure to return if no more data from API
if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []
// For example usage, select a random page
const fakepage = Math.round(Math.random()) * 2
const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
const result = await resultObject.json()
dataIndex.current++;
return {
title: `${dataIndex.current-1}`,
data: await result.data
}
}
const count = () => {
alert(data.length)
}
const checkPageNumbers = () => {
const numbers = data.map(item => parseInt(item.title))
const incremental = [...Array(data.length).keys()]
alert(_.isEqual(numbers, incremental))
}
const getInitialData = async () => {
const list = await fetchData(false)
if(!list) return
setData([list])
setLoading(false)
}
React.useEffect(() => {
getInitialData()
}, [])
const onEndReached = async () => {
const newItems = await fetchData(false)
if(!newItems.data.length) return
setData([...data, newItems])
}
const onRefresh = React.useCallback(async () => {
setShowRefreshingIndicator(true);
const newItems = await fetchData(true)
setData([newItems])
setShowRefreshingIndicator(false)
}, [refreshing]);
if (loading) return <Text>Loading...</Text>
return (
<SafeAreaView style={styles.container}>
<Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
<Button title={"Check length"} onPress={() => count()} />
<SectionList
sections={data}
refreshing={refreshing}
refreshControl={
<RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
}
onEndReached={() => {
if(refreshing) return;
setRefreshing(true)
onEndReached().then(() => {
setRefreshing(false)
})
}}
onEndReachedThreshold={1}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.header}>{title}</Text>
)}
ListFooterComponent={<ActivityIndicator size={"large"} />}
stickySectionHeadersEnabled={false}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 40,
marginHorizontal: 16,
},
item: {
backgroundColor: '#f9c2ff',
padding: 2,
marginVertical: 2,
},
header: {
fontSize: 16,
},
title: {
fontSize: 12,
},
});
Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
if (result.data) {
setProducts([...products, ...results.data]);
}
setIsLoading(false);
};
useEffect(() => {
loadProducts();
}, [])
const onLoadMore = useCallback(() => {
loadProducts();
}
for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback
I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};