I am displaying a list of contacts from my address book in a flatlist and want to be able to search the list. The issue is that initially the list is empty and I get an error because it is trying to filter undefined.
If I type a name though it works and if I delete my search query it then shows all users. I would like it do this from the start. I am not sure why it is undefined intially, perhaps because the state has not yet been set.
const AddContactScreen = ({ navigation }) => {
const [contacts, setContacts] = useState();
const [query, setQuery] = useState("");
const [filteredContactList, setFilteredContactList] = useState(contacts);
useEffect(() => {
(async () => {
const { status } = await Contacts.requestPermissionsAsync();
if (status === "granted") {
const { data } = await Contacts.getContactsAsync({
fields: [Contacts.Fields.PhoneNumbers],
sort: Contacts.SortTypes.FirstName,
});
if (data.length > 0) {
setContacts(data);
}
const newContacts = contacts.filter((item) =>
item.name.includes(query)
);
setFilteredContactList(newContacts);
}
})();
}, [query]);
return (
<Screen>
<FlatList
ListHeaderComponent={
<View style={styles.searchContainer}>
<TextInput
style={styles.searchField}
placeholder="Search"
onChangeText={setQuery}
value={query}
/>
</View>
}
data={filteredContactList}
ItemSeparatorComponent={ListItemSeparator}
keyExtractor={(contact) => contact.id.toString()}
renderItem={({ item }) => (
<ListItem
title={item.name}
onPress={() => console.log("contact selected", item)}
/>
)}
/>
</Screen>
);
};
const styles = StyleSheet.create({
searchContainer: {
padding: 15,
},
searchField: {
borderRadius: 15,
borderColor: "gray",
borderWidth: 1,
padding: 10,
},
});
export default AddContactScreen;
The best practice would be to set your initial value of contacts to an empty array []
So your state would be
const [contacts, setContacts] = useState([]);
const [contacts, setContacts] = useState();
const [query, setQuery] = useState("");
const [filteredContactList, setFilteredContactList] = useState(contacts);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
setLoading(true);
const { status } = await Contacts.requestPermissionsAsync();
if (status === "granted") {
const { data } = await Contacts.getContactsAsync({
fields: [Contacts.Fields.PhoneNumbers],
sort: Contacts.SortTypes.FirstName,
});
setLoading(false);
setContacts(data.length ? data : []);
}
})();
}, [query]);
useEffect(() => {
if(contacts.length) {
const newContacts = contacts.filter((item) =>
item.name.includes(query)
);
setFilteredContactList(newContacts);
}
}, [contacts])
if(loading) return <p>loading...</p>;
if(!contacts.length) return <p>No data found</>;
// your rest of the code.
NOTE: i have written the code from the imagination. You might need to do some tweak.
I would use useEffect when component mounts to get initial contacts and handle the query in a separate function:
useEffect(() => {
getInitialContacts()
}, []);
const getInitialContacts = async () => {
setLoading(true);
const { status } = await Contacts.requestPermissionsAsync();
if (status === "granted") {
const { data } = await Contacts.getContactsAsync({
fields: [Contacts.Fields.PhoneNumbers],
sort: Contacts.SortTypes.FirstName,
});
setLoading(false);
setContacts(data.length ? data : []);
}
});
}
you can use another function that you call when text is entered:
const handleQuery = (query) => {
const newContacts = contacts.filter((item) =>
item.name.includes(query)
);
setFilteredContactList(newContacts);
}
Related
I am using a custom hook useInfiniteFetchSearch to fetch and search data for a infinite scroll component built using react-infinite-scroll-component.
The hook makes an API call and sets the data in the state using setData. Currently, I am using refreshData() method to refresh the data again when an item is deleted from the list.
However, I am not satisfied with this solution as it calls the API again even though I already have the data. Is there a more efficient way to refresh the data and update the infinite scroll component without making another API call?
Here is my custom hook implementation:
import { useState, useEffect, useRef } from "react";
import axios from "axios";
const useInfiniteFetchSearch = (api, resultsPerPage, sort = null) => {
const [data, setData] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(2);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const searchTermRef = useRef(null);
useEffect(() => {
const searchData = async () => {
try {
setLoading(true);
let query = `${api}${
searchTerm === "" ? `?` : `?search=${searchTerm}&`
}page=1`;
query = sort ? `${query}&sort=${sort}` : query;
const result = await axios.post(query);
const fetchedData = result.data;
setData(fetchedData);
setPage(2);
setHasMore(fetchedData.length === resultsPerPage);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
searchData();
}, [searchTerm, api, resultsPerPage, sort]);
const refreshData = async () => {
try {
setLoading(true);
let query = `${api}${
searchTerm === "" ? `?` : `?search=${searchTerm}&`
}page=1`;
query = sort ? `${query}&sort=${sort}` : query;
const result = await axios.post(query);
const fetchedData = result.data;
setData(fetchedData);
setPage(2);
setHasMore(fetchedData.length === resultsPerPage);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
const fetchMore = async () => {
try {
setLoading(true);
let query = `${api}?search=${searchTerm}&page=${page}`;
query = sort ? `${query}&sort=${sort}` : query;
const result = await axios.post(query);
const newData = result.data;
setData((prev) => [...prev, ...newData]);
setPage(page + 1);
setHasMore(newData.length === resultsPerPage);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
const handleSearch = async (e) => {
e.preventDefault();
setSearchTerm(searchTermRef.current.value);
};
const handleDelete = async (e, itemId) => {
try {
await axios.delete(`${api}${itemId}`);
setData((prevData) => prevData.filter((item) => item.id !== itemId));
refreshData();
} catch (error) {
console.log(error);
} finally {
}
};
return {
state: { data, hasMore, loading, searchTermRef, searchTerm },
handlers: {
fetchMore,
setSearchTerm,
handleSearch,
handleDelete,
},
};
};
export default useInfiniteFetchSearch;
I am using this hook in my component:
const { state, handlers } = useInfiniteFetchSearch("/api/guides/search", 5);
const { data, hasMore, loading, searchTermRef, searchTerm } = state;
const { fetchMore, handleSearch, setSearchTerm, handleDelete } = handlers;
....
<InfiniteScroll
dataLength={data.length}
next={fetchMore}
hasMore={hasMore}
scrollableTarget="scrollableDiv"
loader={
<div className="flex justify-center items-center mx-auto">
<Loader />
</div>
}
>
<div className="space-y-1">
{data &&
data.map((item, index) => (
<GuidesItem
key={index}
guide={item}
handleDelete={handleDelete}
/>
))}
</div>
</InfiniteScroll>
I would appreciate any suggestions or solutions to this problem, thank you!
I have this screen in which I want to see ActivityIndicator untill all devices are mapped (not fetched):
const MyScreen = () => {
const [devices, setDevices] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
getDevices();
}, []);
const getDevices = async () => {
const pulledDevices = await fetchDevices();
setDevices(pulledDevices)
setIsLoading(false)
};
if (isLoading)
return (
<ActivityIndicator />
);
return (
<View >
{devices?.map((device) => {
return (
<View>
<Text>{device.name}</Text>
</View>
);
})}
</View>
);
};
Mapping these devices takes some time.
How could I implement here ActivityIndicator untill all devices are mapped.
I suggest you to use a bit more sophisticated async await hook to handle this.
useAsyncHook.js
const useAsync = asyncFunction => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(async () => {
setLoading(true);
setResult(null);
setError(null);
try {
const response = await asyncFunction();
setResult(response);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
}, [asyncFunction]);
useEffect(() => {
execute();
}, [execute]);
return { loading, result, error };
};
This is a raw use async hook that can be enhanced many way but it handles the loading state correctly in this state.
Usage:
const { loading, result, error } = useAsync(yourFunction);
if (loading) return null;
return <Component />;
I am using the meal database.The data from the link is not being updated after search. But if I console.log the search input, I can see the new link.
That's my API for searching:
API_URL_SEARCH="https://www.themealdb.com/api/json/v1/1/search.php?s="
Thats search page:
function Meals({ navigation}) {
const [searchInput, setSearchInput] = useState('');
const handleChange = (inputText) => {
setSearchInput(inputText);
};
const { loading, error, data } = useFetch(config.API_URL_SEARCH + searchInput);
const handleMealSelect = idMeal => {
navigation.navigate("MealDetail", {idMeal})
}
const renderMeals = ({item}) => <Meal meal={item} onSelect={() => handleMealSelect(item.idMeal)}/>
if(loading) {
return <Loading/>;
}
if(error) {
return <Error/>;
}
return(
<View>
<SearchBar
placeholder="Type Here..."
onChangeText={handleChange}
value={searchInput} />
<FlatList keyExtractor={(meals) => meals.id} data={data.meals} renderItem={renderMeals}/>
</View>
)
}
Thats meal component:
const Meal= ({meal, onSelect}) => {
return(
<TouchableOpacity style={styles.container} onPress={onSelect}>
<ImageBackground
style={styles.image}
source={{uri: meal.strMealThumb}}
imageStyle={{borderTopLeftRadius:10, borderTopRightRadius:10}} />
<Text style={styles.title}>{meal.strMeal}</Text>
</TouchableOpacity>
)
}
Here is useFetch for getting data and getting loading and error situations just in case of.
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const fetchData = async () => {
try {
const {data: responseData} = await axios.get(url);
setData(responseData);
setLoading(false); }
catch (error) {
setError(error.message);
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return {error, loading, data};
};
Should I use useRef to save count, page, perPage and update the unified control view rendering.
How to optimize this code in a better way?
function LogTable(props) {
const {queryText, menuKey, parentKey} = props;
const count = React.useRef(1);
const page = React.useRef(0);
const perPage = React.useRef(20);
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [update, setUpdate] = React.useState(0);
const columns = React.useMemo(
() => [
{
Header: 'username',
accessor: 'username',
width: '10%',
Cell: ({value}) => <span>{(value && value.split('#')[0]) || ""}</span>
},
{
Header: 'menu',
accessor: 'menu',
width: '10%',
},
{
Header: 'time',
accessor: 'time',
width: '15%',
},
{
Header: 'operation',
accessor: 'operation',
},
],
[]
);
const updatePage = (newPage) => {
page.current = newPage;
setUpdate(u => u + 1);
};
const updatePerPage = (newPerPage) => {
page.current = 0;
perPage.current = newPerPage;
setUpdate(u => u + 1);
};
useEffect(() => {
if (update !== 0) {
page.current = 0;
setUpdate(u => u + 1);
}
}, [menuKey, parentKey, queryText]);
useEffect(() => {
const fetchData = () => {
setLoading(true);
fetchActions({
url: 'url',
method: 'POST',
body: JSON.stringify({
keyword: queryText,
menu_key: menuKey,
page: page.current + 1,
parent_key: parentKey,
size: perPage.current,
}),
success: body => {
const data = body.data;
count.current = data.logs_count;
setData(data.logs);
},
complete: () => {
setLoading(false);
}
})
};
fetchData();
}, [update]);
return (
<Table data={data} columns={columns} count={count.current} loading={loading} updatePage={updatePage}
updatePerPage={updatePerPage} perPage={perPage.current}/>
)
}
export default LogTable;
With React, the view should flow from the state of the component. Using refs instead of state should only be done when there aren't any other good options. Using a hack like setUpdate(u => u + 1); to force an update right after you've set a ref doesn't help anything - it'd make a whole lot more sense to just use state instead. Performance of the app will be the same, and the code will make a lot more sense.
const { useState } = React;
function LogTable(props) {
const {queryText, menuKey, parentKey} = props;
const [count, setCount] = useState(1);
const [page, setPage] = useState(0);
const [perPage, setPerPage] = useState(20);
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [update, setUpdate] = React.useState(0);
const columns = React.useMemo(
// ...
);
const updatePerPage = (newPerPage) => {
setPerPage(newPerPage);
setPage(0);
};
useEffect(() => {
if (update !== 0) {
setPage(0);
}
}, [menuKey, parentKey, queryText]);
useEffect(() => {
const fetchData = () => {
setLoading(true);
fetchActions({
url: 'url',
method: 'POST',
body: JSON.stringify({
keyword: queryText,
menu_key: menuKey,
page: page + 1,
parent_key: parentKey,
size: perPage,
}),
success: ({ data }) => {
setCount(data.logs_count);
setData(data.logs);
},
complete: () => {
setLoading(false);
}
})
};
fetchData();
}, [update]);
return (
<Table data={data} columns={columns} count={count.current} loading={loading} updatePage={setPage}
updatePerPage={updatePerPage} perPage={perPage.current}/>
)
}
export default LogTable;
It's perfectly fine to call two state setters at once, eg
setPerPage(newPerPage);
setPage(0);
This will result in only a single re-render, not two. (unless there's an effect hook or something observing the changed state variables, which isn't the case here...)
Disclaimer: Please don't mark this as duplicate. I've seen similar questions with answers. But none of them is working for me. I'm just learning React.
What I'm trying to achieve is basically infinite scrolling. So that when a user scrolls to the end of the page, more data will load.
I've used scroll eventListener to achieve this. And it is working.
But I'm facing problems with the state of the variables.
First, I've changed the loading state to true. Then fetch data and set the state to false.
Second, when scrolling to the end of the page occurs, I again change the loading state to true. Add 1 with pageNo. Then again fetch data and set the loading state to false.
The problems are:
loading state somehow remains true.
Changing the pageNo state is not working. pageNo always remains to 1.
And actually none of the states are working as expected.
My goal: (Sequential)
Set loading to true.
Fetch 10 posts from API after component initialization.
Set loading to false.
After the user scrolls end of the page, add 1 with pageNo.
Repeat Step 1 to Step 3 until all posts loaded.
After getting an empty response from API set allPostsLoaded to true.
What I've tried:
I've tried adding all the states into dependencyList array of useEffect hook. But then an infinite loop occurs.
I've also tried adding only pageNo and loading state to the array, but same infinite loop occurs.
Source:
import React, { lazy, useState } from 'react';
import { PostSection } from './Home.styles';
import { BlogPost } from '../../models/BlogPost';
import { PostService } from '../../services/PostService';
const defaultPosts: BlogPost[] = [{
Id: 'asdfg',
Content: 'Hi, this is demo content',
Title: 'Demo title',
sections: [],
subTitle: '',
ReadTime: 1,
CreatedDate: new Date()
}];
const defaultPageNo = 1;
const PostCardComponent = lazy(() => import('./../PostCard/PostCard'));
const postService = new PostService();
const Home = (props: any) => {
const [posts, setPosts]: [BlogPost[], (posts: BlogPost[]) => void] = useState(defaultPosts);
const [pageNo, setPageNo] = useState(defaultPageNo);
const [pageSize, setPageSize] = useState(10);
const [loading, setLoading] = useState(false);
const [allPostsLoaded, setAllPostsLoaded] = useState(false);
const [featuredPost, setFeaturedPost]: [BlogPost, (featuredPost: BlogPost) => void] = useState(defaultPosts[0]);
async function getPosts() {
return await postService.getPosts(pageSize, pageNo);
}
async function getFeaturedPost() {
return await postService.getFeaturedPost();
}
function handleScroll(event: any) {
console.log('loading ' + loading);
console.log('allPostsLoaded ' + allPostsLoaded);
var target = event.target.scrollingElement;
if (!loading && !allPostsLoaded && target.scrollTop + target.clientHeight === target.scrollHeight) {
setLoading(true);
setPageNo(pageNo => pageNo + 1);
setTimeout(()=>{
getPosts()
.then(response => {
const newPosts = response.data.data;
setLoading(false);
if (newPosts.length) {
const temp = [ ...posts ];
newPosts.forEach(post => !temp.map(m => m.Id).includes(post.Id) ? temp.push(post) : null);
setPosts(temp);
} else {
setAllPostsLoaded(true);
}
})
}, 1000);
}
}
function init() {
setLoading(true);
Promise.all([getFeaturedPost(), getPosts()])
.then(
responses => {
setLoading(false);
setFeaturedPost(responses[0].data.data);
setPosts(responses[1].data.data);
}
);
}
React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
init();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []
);
return (
<PostSection className="px-3 py-5 p-md-5">
<div className="container">
<div className="item mb-5">
{posts.map(post => (
<PostCardComponent
key={post.Id}
Title={post.Title}
intro={post.Content}
Id={post.Id}
ReadTime={post.ReadTime}
CreatedDate={post.CreatedDate}
/>
))}
</div>
</div>
</PostSection>
);
};
export default Home;
Used more effects to handle the change of pageNo, loader and allPostsLoaded state worked for me.
Updated Source:
import React, { lazy, useState } from 'react';
import { Guid } from "guid-typescript";
import { PostSection } from './Home.styles';
import { BlogPost } from '../../models/BlogPost';
import { PostService } from '../../services/PostService';
import { Skeleton } from 'antd';
const defaultPosts: BlogPost[] = [{
Id: '456858568568568',
Content: 'Hi, this is demo content. There could have been much more content.',
Title: 'This is a demo title',
sections: [],
subTitle: '',
ReadTime: 1,
CreatedDate: new Date()
}];
const defaultPageNo = 1;
const defaultPageSize = 10;
const PostCardComponent = lazy(() => import('./../PostCard/PostCard'));
const postService = new PostService();
const Home: React.FC<any> = props => {
const [posts, setPosts]: [BlogPost[], (posts: BlogPost[]) => void] = useState(defaultPosts);
const [pageNo, setPageNo] = useState(defaultPageNo);
const [pageSize, setPageSize] = useState(defaultPageSize);
const [loading, setLoading] = useState(false);
const [allPostsLoaded, setAllPostsLoaded] = useState(false);
const [featuredPost, setFeaturedPost]: [BlogPost, (featuredPost: BlogPost) => void] = useState(defaultPosts[0]);
function getNewGuid() {
return Guid.create().toString();
}
async function getPosts() {
return await postService.getPosts(pageSize, pageNo);
}
async function getFeaturedPost() {
return await postService.getFeaturedPost();
}
function init() {
setLoading(true);
Promise.all([getFeaturedPost(), getPosts()])
.then(
responses => {
setLoading(false);
setFeaturedPost(responses[0].data.data);
setPosts(responses[1].data.data);
}
);
}
React.useEffect(() => {
init();
return;
}, []);
React.useEffect(() => {
if (allPostsLoaded || loading) return;
function handleScroll(event: any) {
var target = event.target.scrollingElement;
if (!loading && !allPostsLoaded && target.scrollTop + target.clientHeight === target.scrollHeight) {
setPageNo(pageNo => pageNo+1);
}
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [loading, allPostsLoaded]
);
React.useEffect(() => {
if (pageNo > 1) {
setLoading(true);
setTimeout(()=>{
getPosts()
.then(response => {
const newPosts = response.data.data;
setTimeout(()=>{
setLoading(false);
if (newPosts.length) {
const temp = [ ...posts ];
newPosts.forEach(post => !temp.map(m => m.Id).includes(post.Id) ? temp.push(post) : null);
setPosts(temp);
} else {
setAllPostsLoaded(true);
}
}, 1000);
})
}, 1000);
}
}, [pageNo]
);
return (
<PostSection className="px-3 py-5 p-md-5">
<div className="container">
<div className="item mb-5">
{posts.map(post => (
<PostCardComponent
key={post.Id}
Title={post.Title}
intro={post.Content}
Id={post.Id}
ReadTime={post.ReadTime}
CreatedDate={post.CreatedDate}
/>
))}
</div>
</div>
</PostSection>
);
};
export default Home;