Better understand debounceTime (RXJS) for an API request - reactjs

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>
);
};

Related

React with Firestore autocomplete

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.

get current useState values inside functions (w/ short, explicit codepen)

I understand (somewhat) how to use useEffect to update state, but I struggle with situations like when you need current state inside of another function, before the "nextTick" as it were.
Here is a simple Codepen with the exact issue. Make sure the Pen console is open.
https://codepen.io/kirkbross/pen/vYRNpqG?editors=1111
const App = () => {
const [state, setState] = React.useState(null);
// how can I make sure the below function knows what the current state really is?
const handleAppend = (state) => {
console.log("click");
console.log(state?.text + " foobar");
};
return (
<div class="app">
<div className="row">
<span>Text: </span>
<input
type="text"
onChange={() => setState({ text: e.target.value })}
/>
</div>
<div className="row">
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
</div>
);
};
You're shadowing your state variable in your handleAppend function. You don't need to pass in an argument since state is available in scope of the component
const handleAppend = () => {
console.log("click");
console.log(state?.text + " foobar");
};
I did some changes. You dont need to use ur state as a parameter, since your textState lives inside your app component and there for you can reach it within your function.
Also, i changed the state and setState to textState, setTextState to make it less confusing. Also after clicking on the button and console logging, i cleared the textState so the next value wont be effected. Check it out below.
function App() {
const [textState, setTextState] = React.useState(null);
const handleAppend = () => {
console.log("click");
console.log(textState + " foobar");
setTextState('')
//also, you could make the input box clear after each press on button by adding value={textState} in the input.
};
return (
<div className="App">
<input
type="text"
onChange={(e) => setTextState(e.target.value)}
/>
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
);
}
My real world case was more complicated than the Pen. The actual function needing state was a useCallback function and I had forgotten to add state to the dep array of the useCallback function.
const handleDragEnd = useCallback(
async (result) => {
const { source, destination, draggableId } = result;
console.log(state); // shows up now.
},
[state], // I had forgotten to add state to the useCallback dep array
);

Clear form input fields after submit it to firestore?

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!

React.js Virgin: Why is my hard coded data being passed differently than data entered by user?

This is code I collected from a video to learn react with hooks. It's supposed to create and name 3 timers from data hard coded and allow the user to create/name new timers. My code isn't exactly the same as the video because I couldn't get his to work at all.
The Problem is: I can get it to display the name for the user input timer or the hard coded timers, but not both. Right now it will display the name for the user input timer, but not the hard coded ones.
import Timer from './timer'
import { useState } from 'react'
const TimerManager = () => {
const [name, setName] = useState("")
const [timerNames, setTimerNames] = useState([
"Medication",
"Chicken",
"Tee"
])
return(
<div>
<form onSubmit={(e) => {
e.preventDefault()
const copyOfTimerNames = [...timerNames]
copyOfTimerNames.push({name})
setTimerNames(copyOfTimerNames)
setName("")
}}>
<input value={name}
onChange={(event) => {setName(event.target.value)}}
/>
<button>Add New Timer</button>
</form>
{timerNames.map(timerName => (
<Timer name={timerName} />
))}
</div>
);
}
export default TimerManager
No real changes to except the use of e.preventDefault() instead of event.preventDefault()
import React from 'react'
import { useState, useEffect } from "react"
const Timer = ({name}) =>{
const [seconds, setSeconds] = useState('60');
const [isCountdownRunning, setIsCountdownRunning] = useState(false)
const [shouldAlert, setShouldAlert] = useState(false)
console.log("Timer.render seconds", seconds)
console.log("name", name)
const secondsLeft = parseInt(seconds)
useEffect(() =>{
if(isCountdownRunning && secondsLeft > 0){
setTimeout(() =>{
setSeconds(secondsLeft - 1)
}, 1000)
} else {
setIsCountdownRunning(false)
if(shouldAlert){
alert(`The ${name.name} Timer is done!`)
setShouldAlert(false)
}
}
},[ isCountdownRunning, secondsLeft])
return(
<div className="Timer">
This is the {name.name}
<input disabled={isCountdownRunning}
value={seconds}
onChange={(event) => {setSeconds(event.target.value)}}
type="number"
></input>
<button
onClick={() => { setShouldAlert(true)
setIsCountdownRunning(true)
}}>
Start Countdown
</button>
</div>
)
}
export default Timer
In timer.js if I used just ${name} the code would crash when I created a new timer and it would show [Object object] in the alert, but it would display the names of the hard coded timers.
The issue is when you are pushing in user entered names you are creating objects versus just pushing the string literal.
copyOfTimerNames.push({ name }) // <-- i.e. { name: "Bill" }
Solution: just push in the entered value.
<form onSubmit={(e) => {
e.preventDefault();
const copyOfTimerNames = [...timerNames];
copyOfTimerNames.push(name); // <-- push string, i.e. "Bill"
setTimerNames(copyOfTimerNames);
setName("");
}}>
Even better, use functional state update and array concatenation. Here names is the current state, and it is spread into a new array with the new name appended at the end. Cleaner and more succinct code.
<form onSubmit={(e) => {
e.preventDefault();
setTimerNames(names => [...names, name]);
setName("");
}}>
Your initial state is array of strings. And in your onSubmit you're updating your state with an object.
This is supposed to be a string.
// WRONG
copyOfTimerNames.push({name})
// RIGHT
copyOfTimerNames.push(name)

How can I display my API Data from search in React?

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

Resources