Using ReactJS, Firestore - Firebase v9.
I have an autocomplete search bar on my web, built with just pure React. The question that I am trying to solve for at least one month is, if I can make this autocomplete input work with Firestore - (user type E.g 'elepha', auto suggestion appears with offer with word elephant - this is what I coded, and with same case, user will click on the suggestion of elephant, and this word elephant will be send to Firestore.)
Cuz there is not any solution on internet, I wonder, if my question is even possible to make, or not.
My simple autocomplete bar code - (animals_data stands for file with animals names)
and I tried to add onClick={pleaseSend} which is basic addDoc function, but when I click on the suggestion, in Firestore will only appear blank input "".
<SearchBar data={animals_data} />
And the filtering code:
function SearchBar({ placeholder, data }) {
const [filteredData, setFilteredData] = useState([]);
const [wordEntered, setWordEntered] = useState("");
const [newAnswer, setAnswer] = useState("")
const [users, setUsers] = useState([]);
const usersCollectionRef = collection(db, "Answers")
const createUser = async () => {
await addDoc(usersCollectionRef, {name: newAnswer}).then(()=>{
window.location.reload()
}).catch((err)=>{
console.log(err)
})
};
const handleFilter = (event) => {
const searchWord = event.target.value;
setWordEntered(searchWord);
const newFilter = data.filter((value) => {
return value.full_name.toLowerCase().includes(searchWord.toLowerCase());
});
if (searchWord === "") {
setFilteredData([]);
} else {
setFilteredData(newFilter);
}
};
const clearInput = () => {
setFilteredData([]);
setWordEntered("");
};
return (
<div className="search">
<div className="searchInputs">
<input
type="text"
placeholder={placeholder}
value={wordEntered}
onChange={handleFilter}
/>
</div>
{filteredData.length !== 0 && (
<div className="dataResult">
{filteredData.slice(0, 15).map((value, key) => {
return (
<a className="dataItem" onClick={createUser} target="_blank">
<p>{value.full_name} </p>
</a>
);
})}
</div>
)}
</div>
);
}
export default SearchBar;
EDIT 1: added code screenshots
web:
Thank you very much for reading.
after analyzing your code, I notice that you don't update the value newAnswer using its setter. Therefore, you should use the setter to update the state on user click and then add the firestorm document. You can do that by either using a button/unmodifiable input field in instead of an anchor tag to store the value of each option, then use this value inside the click handler to update the state and then use a useEffect to update firestore every time the state changes. Let me know if you need help with some code. Please post it below your original question as an edit without changing it.
Related
I'm trying to create a simple CRUD application with react and Firestore (new to both).
I never used react with db, so previously I would simply set the state of title and content to "" after submit, but that's create a loop with Firestore onSnapshot. I could wrap it in form and use preventDefault(), but (possibly my mistake) it created strange outcome unless I left behind async. I'm thinking about this issue for a few days and yet to find the right answer to it. Maybe I could prevent Firestore to accept and save empty strings? That cant be a reasonable answer as it would create an extra loop front to back for no other reason than my shortcoming. I think the answer should be some sort of a conditionally state setting, but that's just a guess again. Now I`ve watched quite a few hours a tutorials on youtube, but they all leave out this issue, which I think its a bit silly. I would like to find the answer to it as even though I might could come up with something that will work, but I really like to see the common use case of it which defo will be more elegant than my trial and error solution. Thank you in advance!
function App() {
const [newTitle, setNewTitle] = useState("")
const [newContent, setNewContent] = useState("")
const [users, setUsers] = useState([]);
const usersCollectionRef = collection(db, "users")
useEffect(
() =>
onSnapshot(collection(db, "users"), (snapshot) =>
setUsers(snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })))
),
[]
);
const createEntry = async () => {
await addDoc(usersCollectionRef, { Title: newTitle, Content: newContent })
}
return (
<div>
<input type="text" placeholder="Title" onChange={event => { setNewTitle(event.target.value)} }/>
<textarea type="text" placeholder="Content" onChange={event => { setNewContent(event.target.value) } }/>
<button onClick={createEntry}> Add </button>
{users.map((user) => {
return (
<div className="note">
<h1> {user.Title} </h1>
<p>{user.Content}</p>
</div>
)
})}
</div>
);
}
Took me forever to figure out what Im doing wrong, but seems like it finally I overcome it. Now took my answer with a pinch of salt as Im hoping it will be helpful for you, but might not all my "theories" will be 100% correct. (PS: changed users to notes as it makes a lot more sense)
While I was studying I remembered a project which exactly the same as this, but without firestore backend. I reused that project and done a lot of trial and error to find the solution.
I did remember that on that project it was broken down to components and we passed back the relevant info to the app.jsx. But while i was thinking i realised that we actually done more than that.
I think might be the issue that here I used 3 different states.
const [newTitle, setNewTitle] = useState("")
const [newContent, setNewContent] = useState("")
const [users, setUsers] = useState([]);
Now its all nice and functional, but uncontrolled I believe? Now what we have done in the previous project that we actually stored the individual user entries in and object and passed back the id`s to the app.js. While we were doing it we reset the state of the object fields to "".
const [note, setNote] = useState({
title:"",
content:""
});
I think might be the reason for this to work now that we have objects in an array, not individual strings seemingly not related to each other. ? Its a guess from my side.
app.jsx
const [notes, setNotes] = useState([]);
const usersCollectionRef = collection(db, "notes")
const createEntry = async (note) => {
await addDoc(usersCollectionRef, { title: note.title, content: note.content })
}
return (
<div>
<CreateArea onAdd={createEntry} />
{notes.map((noteItem) => {
return (
<Note
key={noteItem.id}
id={noteItem.id}
title={noteItem.title}
content={noteItem.content}
onDelete={deleteNote}
/>
);
})}
</div>
);
}
createArea.jsx
function CreateArea(props) {
const [note, setNote] = useState({
title: "",
content: ""
});
function handleChange(event) {
const { name, value } = event.target;
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(event) {
props.onAdd(note);
setNote({
title: "",
content: ""
});
event.preventDefault();
}
return (
<div className="create-area">
<form>
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
<textarea
name="content"
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows="3"
/>
<button onClick={submitNote}>Add</button>
</form>
</div>
);
}
By doing this I use the spread operator to handle the value changes and until it is submitted it wont be reset to "". I think the issue might have been that the i would have needed 3 state to do this initially.
first state: ""
second state: handles the input changes
third state: submitting and reset value to ""
Doing this with my original states wouldnt be logically possible i think so one extra step needed. On submission it sends back the note where we have access to its fields to save it. Its probably not a biggie for some, but Im happy that i find solution on my own, although it took me longer than i expected. I hope it can be helpful for someone. Please note that all the "ideas" why it didn`t work are ideas only. If you have anything to add or correct me you are more than welcome to! Thank you!
I'm new to React, been working on it for the past week. I'm trying to make a simple app that has a 'product create' form and a list of products with a search bar (using Input component from antd); in the list I can click on any product to open the details page.
Right now I'm blocked by some not properly working logic or something I miss. When I tried the Input onChange with an Array I created in the code it worked fine, but now that I'm using a mock api (from fakestoreapi.com to be precise), I can't make it work.
ProductsList.tsx
function ProductsList() {
const [list, setList] = useState<Array<Product>>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { // I think something is wrong here
ProductService.getAll()
.then((res: any) => {
setList(res.data);
setLoading(false);
})
.catch((e: Error) => console.log(e));
}, []); // tried: 'query' const from state, 'filterList' from state
function onChange(e: React.ChangeEvent<HTMLInputElement>) { // Or here (or both here and in useEffect)
console.log('in onChange');
const filterList: Array<Product> = list.filter((item) =>
item.title.toLowerCase().startsWith(e.target.value.toLowerCase())
);
setList(filterList);
}
return (
<div>
<Spin spinning={loading}>
<List
header={
<Input
type="text"
placeholder="Search product"
allowClear
onChange={onChange}
/>
}
split
dataSource={list}
renderItem={(item) => (
<List.Item key={item.id}>
<Link to={`/products/${item.id}`}>{item.title}</Link>
</List.Item>
)}
></List>
</Spin>
</div>
);
}
export default ProductsList;
I tried adding some dependencies to the useEffect hook, but maybe they were the wrong ones. As I said, with a local array this worked, but now after loading the full list once, when I get to the Input and search something, the list is deleted. I think I spotted the problem in the fact that I don't reset the list to the full one, but I don't actually know how to do that (that's why I'm here). I tried to search something online but except for dependencies, I didn't find something specific to help me.
If needed, here is the ProductService.getAll() function:
function getAll() { // http is axios
return http.get<Array<Product>>(`/products`);
}
I'll be glad to add everything that could be helpful if needed.
const [list, setList] = useState<Array<Product>>([]); // The full list
const [filteredList, setFilteredList] = useState<Array<Product>>([]); // the list you display
function onChange(e: React.ChangeEvent<HTMLInputElement>) { // Or here (or both here and in useEffect)
console.log('in onChange');
const temp: Array<Product> = list.filter((item) => //keep the filter on the full list but only display the filtered list
item.title.toLowerCase().startsWith(e.target.value.toLowerCase())
);
setFilteredList(temp);
}
//the datasource: dataSource={filteredList}
I'm still unexperienced with react so that even after searching for a solution and finding some pointers I still cant grasp what the problem is or how to solve it.
I have a Component that renders a list of images. It also contains a search input. I copy the search input onChanged to the state. If onKeyPressed is the return key or when the search button is pressed, that text is again copied from state to the state.searchTerm. The search itself is an effect that watches for changes in searchTerm an then executes a search, updating the list of images. However I feel like, because I change the state with every onChange in the search input, I trigger a re-render of the entire component including the list of images which is just annoying. How can I get rid of this?
I tried to shrink my styled and dynamic code to a minimal working version. What would be the best way to solve this? Would it help to split list and search into separate components with individual state with the parent just holding the list of assets, passing it to the list child and the search child getting a reference to onSearch?
But then I just move the problem since if the search child re-renders, the parent will as well, right?
function AssetListTool ({}) {
const [assets, setAssets] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [searchText, setSearchText] = useState('');
const params = {
limit: 30,
sort: 'title:desc',
searchTerm
};
const Asset = memo(function ({asset }) {
return <>
<div className="asset">
<img src={asset.thumbnail} />
</div>
</>;
});
useEffect(() => {
const matchingAssets = assetApi.getAllAssets({...params });
Promise.all([matchingAssets],
).then(responses => {
setAssets(assets.concat(responses[0].items));
});
}, [searchTerm]);
const onSearchTextChanged = useCallback((event) => {
setSearchText(event.target.value);
}, [searchText]);
function onSearchKeyPressed(event) {
if (event.key === 'Enter') {
onSearch();
}
}
function onSearch() {
setAssets([]);
setSearchTerm(searchText);
}
return (
<>
<div>
<div>
<input onChange={onSearchTextChanged} onKeyPress={onSearchKeyPressed} value={searchText}
type="text"/>
</div>
<div>
<button onClick={onSearch}>
<Icon icon={faSearch}/>
</button>
</div>
</div>
<div>
{assets && assets.length > 0 &&
<div>
{assets.map((asset) => <Asset asset={asset}/>)}
</div>
}
</div>
</>
);
}
export default AssetListTool;
Ah, should've searched just a little more and I was on the right track... The solution is to not touch the state for the text input and instead use a reference to it which is then read for the search as was explained here:
Getting input values without rerender
Thought about a local variable but that didnt work and using event.target.value was always missing the last input. So ref is the trick here..
So I'm having trouble writing the function to display the API data whenever I search something. I tried to follow tutorials online, but they are using different API's, so the function and setup is completely different from mine.
I have everything working as far as displaying data. I only need my searchCoin function to showcase whatever coin I look up and to display it.
From the API, the name and id show the exact same thing, so you could target the id or name to find the correct coin
Here is my code
function App() {
const [coins, setCoins] = useState([]);
const [search, setSearch] = useState('');
useEffect(() => {
axios
.get(
'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=3&page=1&sparkline=false'
)
.then(res => {
setCoins(res.data);
console.log(res.data);
})
.catch(error => console.log(error));
}, []);
const handleChange = e => {
setSearch(e.target.value);
};
const searchCoin = e => {
e.preventDefault();
I'm trying to fix this code below to work properly
// setSearch(
coins.filter(coin =>
coin.name.toLowerCase().includes(search.toLowerCase())
)
);
};
return (
<div className='coin-app'>
<div className='coin-search'>
<h1>Search a currency</h1>
<form onSubmit={searchCoin}>
<input type='text' onChange={handleChange} />
<button>Search</button>
</form>
</div>
{coins.map(coin => {
return (
<Coin
key={coin.id}
name={coin.name}
price={coin.current_price}
symbol={coin.symbol}
marketcap={coin.total_volume}
volume={coin.market_cap}
image={coin.image}
/>
);
})}
</div>
);
}
React Only Updates What’s Necessary
React DOM compares the element and its children to the previous one, and only applies the DOM updates necessary to bring the DOM to the
desired state. #reactjs.org
Therefore, every time when coins or search change, the App will re-render.
so we can just create a variable that filters using the search value, creating a variable FilteredCoins
const filteredCoins = coins.filter((coin) =>
coin.name.toLowerCase().includes(search.toLowerCase())
Notice that we are using search, which will be different every time the state change.
Then you can just iterate filteredCoins in this manner:
{filteredCoins.map((coin) => (
<Coin
key={coin.id}
name={coin.name}
price={coin.current_price}
symbol={coin.symbol}
marketcap={coin.total_volume}
volume={coin.market_cap}
image={coin.image}
/>
))}
sandbox to demonstrate:
https://codesandbox.io/s/coins-8ukzf
Here are the steps you need to do.
Add another state hook to hold the filtered coins list
const [filteredCoins, setFilteredCoins] = useState(coins);
add a useEffect to update the filteredCoins once the coins have been populated
useEffect(() => {
setFilteredCoins(coins)
} ,[coins])
Use the filteredCoins in your map
{filteredCoins.map((coin) => {
Set a useEffect to update the filteredCoins from the original coins on every search change
useEffect(() => {
setFilteredCoins(
coins.filter(coin =>
coin.name.toLowerCase().includes(search.toLowerCase())
)
)
} ,[search])
Or run it in your searchCoin function like this
const searchCoin = e => {
e.preventDefault();
I'm trying to fix this code below to work properly
setCoins(
coins.filter(coin =>
coin.name.toLowerCase().includes(search.toLowerCase())
)
);
};
This will run everytime search is updated and filter out only the ones that match
I'm trying to create a search engine (input) where each time a user starts typing, it'll wait a bit before sending a query call. From my understanding, we would import
import { of } from "rxjs";
import { debounceTime } from "rxjs/operators";
The purpose of using of is because we will be using a string. As for debounceTime, we want to only process (make a request) to the last input that has been written in the set amount of time we pick. For this implementation, the way I have it set is
const PostTypeSelection = ({ client }) => {
const [search, updateSearch] = useState("");
const [tags, updateTags] = useState([]);
const searchObserver = of(search);
const handleChange = e => {
const { value } = e.target;
updateSearch(value);
searchObserver
.pipe(
debounceTime(1000),
distinctUntilChanged()
)
.subscribe(x => console.log("we run query here", x));
}
return (
<div>
<p>Select tags you'll want to see</p>
<input
type="text"
name="search"
placeholder="Type tag name"
onChange={handleChange}
/>
{tags.map(tag => {
return (
<div key={tag.id}>
{tag.name} ({tag.count})
</div>
);
})}
</div>
);
};
My main question is what is wrong with my implementation? There is no delay when I start typing and the console.log get run immediately. Another problem I noticed is if I tried to use delay, it doesn't remove previous request just sends them all after a set amount of time.
All help is appreciated especially in better understanding how to use an Observer and better understand the right use of debounceTime
Take what I am going to say with a grain of salt. I am not a Rxjs developer (being honest I have never used the lib 🙂) but I get the concepts.
I see two problems in you implementation:
For every keypress you are creating and new observer and subscripting to it, because all the pipe and subscribe code is in the handler.
From what the documentation says about of, it emits the provided values in a sequence. For example: of(1,2,3,3). Here I don't think you have a sequence. Every time you update the state the whole function runs resulting in of begin called once with every value: of('a'), of('ab'), of('abc'), not of('a', 'ab', 'abc').
This is what I did to make it work:
const PostTypeSelection = ({ client }) => {
const [tags, updateTags] = useState([]);
const searchObserver = new Subject().pipe(
debounceTime(1000),
distinctUntilChanged()
);
searchObserver.subscribe(x => console.log("we run query here", x))
const handleChange = e => {
const { value } = e.target;
searchObserver.next(value)
}
return (
<div>
<p>Select tags you'll want to see</p>
<input
type="text"
name="search"
placeholder="Type tag name"
onChange={handleChange}
/>
{tags.map(tag => {
return (
<div key={tag.id}>
{tag.name} ({tag.count})
</div>
);
})}
</div>
);
};