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!
Related
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.
So, I am trying to filter and map an array from the GIPHY api using stored variables in the useState hook.
Here's my code
const [gifdata, setGifdata] = useState([])
const [Search, setSearch] = useState("")
function handleChange(e) {
setSearch(e.target.value)
}
useEffect(()=> {
axios.get(`https://api.giphy.com/v1/gifs/trending?api_key=nKEFKPSILLeIlqLEjqhVsRO8ShxIjfcn&limit=50&rating=g`)
.then(res=>{
setGifdata(res.data)
console.log(res.data)
})
}, [])
const filteringSearch = gifdata.filter(gif=>
gif.title.toLowerCase().includes(Search.toLowerCase()))
return (
<div>
<header className="bg-blue-600">
<div className="logo">
<label htmlFor="logo">DejareX</label>
</div>
</header>
<div className="heroSection mx-auto">
<h1>GIF Collections at it's peak</h1>
<p>loremipsum blah blah blah</p>
<input type="text" placeholder="Search For A GIF" onChange = {handleChange} />
{filteringSearch.map(gif => {
return (
<Gif
key = {gif.id}
gifImgSrc = {gif.images.original.webp}
description = {gif.title}
/>
)
})}
</div>
</div>
)
}
NOTE: CREATED A RANDOMEMAIL ADDRESS TO GET THIS API KEY, Its not for production.
I am new to react, please try put me through as easy as possible
Someone said the data from the api is probably not an array, i rechecked and it seems like it is true. Cause it first returns an object before getting into the array, who can help with fixing that please
As I said, res.data is not an array. axios adds another data layer to the result. Therefore your res.data is not the same as you see in the browser, in fact it is:
{data: Array(50), pagination: Object, meta: Object}
Therefore, changing res.data to res.data.data will solve the issue
Here is a dummy Live Demo
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..
I've been banging my head against my keyboard for the past 8 hours trying to figure out why each input field is showing different values for the same state.
Here is the code -
const BeaconSettingsCard = (props) => {
const [settingsItems, setSettingsItems] = useState([]);
const handleAddBeaconBtnOnClick = () => {
const id = settingsItems.length;
const newItem = (
<InputItem
id={id}
key={id}
type="InputField"
title="Test Title"
value="Test Value"
onChange={(e) => handleBeaconIdInputFieldOnChange(e, id)}
/>
);
setSettingsItems((settingsItems) => [...settingsItems, newItem]);
};
const handleBeaconIdInputFieldOnChange = (e, id) => {
console.log("settingsItems: ", settingsItems); // each input field shows a different settingsItems value ??
};
let cardHeaderButton = (
<InputItem type="Button" width="150px" onClick={handleAddBeaconBtnOnClick}>
Click to Add
</InputItem>
);
return (
<SettingsCard
headerButton={cardHeaderButton}
settingsItems={settingsItems}
/>
);
};
export default BeaconSettingsCard;
When I log the "settingsItems" state in the onChange event for each input field, I get different values.
On the first dynamically generated inputfield, it logs settingsItems as []. On the second, it logs [{React Component}]. On the third, it logs [{React Component}, {React Component}] and so forth.
These should all be logging the same state value! My friend who is a react wiz couldn't seem to figure this out either. Really hoping someone here can. Thank you.
I solved this strange issue by converting the code from React hooks to a normal React component.
Not ideal, but works. Hopefully if someone runs into a strange issue like this, this solution will work for you too.
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>
);
};