I am creating a React App that makes search request to server as the user types. I want to debounce this search request, but not sure how to implement it in my existing code:
Mobx Store:
// function which initiates a fetch request to server
#action searchPlanet = async (event) => {
this.searchString = event.target.value;
this.planets = await getPlanets(this.searchString);
}
React Component calling searchPlanet:
const Search = observer(({ store }) => {
const planetList = toJS(store.planets);
return (
<div>
<div className={style.search_container}>
<input type="text" id="search" onChange={e => store.searchPlanet(e)} value={store.searchString} placeholder="search planet" />
</div>
</div>
)
})
I can't use debounce function directly on onChange because that will also delay the re-rendering of Search component, so the user will see the typed text after some time. But I am not able to figure out to how to implement debounce function in my store? I can do something like:
import _ from lodash
#action searchPlanet = async (event) => {
this.searchString = event.target.value;
this.planets = await getPlanets(this.searchString);
}
debounceSearch = _.debounce(this.searchPlanet, 250);
The issue with this is that I can't call debounceSearch directly from Search component because of reason mentioned above. But I want to debounce getPlanets function, which returns a promise (I am not sure if Lodash debounce function can return the promise returned by the wrapped function)?
Instead of assigning a value to planets in your searchPlanet action, you could do it in the debounced function instead.
Example
#observer
class App extends Component {
#observable value = "";
#observable query = "";
onChange = action(event => {
const { value } = event.target;
this.value = value;
this.search(value);
});
search = debounce(action(query => {
this.query = query;
}), 250);
render() {
const { value, query, onChange } = this;
return (
<div>
<input value={value} onChange={onChange} />
<div>{query}</div>
</div>
);
}
}
Related
From the extent of my knowledge, action in mobx is supposed to cause the observer to rerender, right? However, even though I'm invoking action on the handleSubmit method in my AddTask component, it does not rerender the observer(TaskList). Do I have to wrap AddTask in an observable as well? But when I tried that, it didn't render any of the data at all. I'm genuinely perplexed and have tried so many different things for hours. Please help.
AddTask:
export default function AddTask() {
const [task, setTask] = useState('');
const handleSubmit = action(async (event: any) => {
event.preventDefault();
try {
const response = await axios.post('http://localhost:5000/test', { task });
} catch (error: Error | any) {
console.log(error);
}
});
const onChange = (e: any) => {
const value = e.target.value;
if (value === null || value === undefined || value === '') {
return;
}
setTask(value);
};
return (
<div className="task">
<form onSubmit={handleSubmit}>
<input type="text" name="task" value={task} onChange={onChange}></input>
<br></br>
<input type="submit" name="submit" value="Submit" />
</form>
</div>
);
}
TaskList:
const TaskList = () => {
const [update, setUpdate] = useState<string>('');
useEffect(() => {
TaskStore.fetchTasks();
}, []);
const onChangeValue = (e: any) => {
setUpdate(e.target.value);
};
return (
<div>
<p>
update input <input onChange={onChangeValue} value={update} />
</p>
{TaskStore.tasks.map((value: any, key) => {
console.log(value);
return (
<div key={key}>
<p>
{value.task}
<DeleteTask value={value} taskList={TaskStore} />
<UpdateTask value={update} current={value} taskList={TaskStore} />
</p>
</div>
);
})}
</div>
);
};
export default observer(TaskList);
taskStore:
interface Task {
task: string;
}
class TaskStore {
constructor() {
makeAutoObservable(this);
}
tasks = [] as Task[];
#action fetchTasks = async () => {
try {
const response: any = await axios.get('http://localhost:5000/test');
this.tasks.push(...response.data.recordset);
} catch (error) {
console.error(error);
}
};
}
export default new TaskStore();
As far as I can see you are not doing anything in your submit action to handle new task.
You are making request to create new task, but then nothing, you don't do anything to actually add this task to your store on the client nor make store refetch tasks.
const handleSubmit = action(async (event: any) => {
event.preventDefault();
try {
// Request to create new task, all good
const response = await axios.post('http://localhost:5000/test', { task
// Now you need to do something
// 1) Either just add task to the store manually
// Add some action to the store to handle just one task
TaskStore.addTask(response.data)
// 2) Or you can do lazy thing and just make store refetch everything :)
// Don't forget to adjust this action to clear old tasks first
TaskStore.fetchTasks()
});
} catch (error: Error | any) {
console.log(error);
}
});
P.S. your action decorators are a bit useless, because you cannot use actions like that for async function, for more info you can read my other answer here MobX: Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed
I created a simple "notes app" by just passing the props and callback functions to the child and nested child components. My CRUD is working fine when I update the note on each keystroke. However, when I call the API using the Debouncing concept, the App.js forgets the state and re-initiates it to the default value.
here is the following code -
App.js
const addNote = async (note) => {
const newNote = await CreateNote(note); // this is API call
let newNotes = [...notes]; // notes is state - array of note object
newNotes.unshift(newNote);
setNotes(newNotes);
setActiveNote(newNote);
};
note-editor.js
const handleNoteChange = (e) => {
let newNote = { ...activeNote, [e.target.name]: e.target.value };
activateNote(newNote);
//addOrUpdateNote(newNote); // this code is working and updating the list correctly
optimizedAddOrUpdateNote(newNote); // this code re-initiates the "notes" state in App.js to default []
};
const addOrUpdateNote = (note) => {
if (!note.createdDate) {
if (note.title.trim() || note.body.trim()) {
addNote(note); // this is coming from app.js as prop callback
}
} else {
updateNote(note); // this is coming from app.js as prop callback
}
};
const debounce = (func) => {
let timer;
return function(...args) {
const context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
func.apply(context, args);
}, 500);
}
}
const optimizedAddOrUpdateNote = useCallback(debounce(addOrUpdateNote), []);
return (
<div className={Classes["note-editor-body"]}>
<input
type='text'
name='title'
placeholder='title...'
onChange={handleNoteChange} //trying to call the API using debounciing
value={activeNote.title}
/>
<textarea
maxLength={AppConstants.NOTE_BODY_CHARACTER_LIMIT}
name='body'
placeholder='add your notes here'
onChange={handleNoteChange} //trying to call the API using debounciing
value={activeNote.body.slice(
0,
AppConstants.NOTE_BODY_CHARACTER_LIMIT
)}
/>
</div>
)
Any help would be appreciated. Thanks!
React has its own build in debounce functionality with useDeferredValue(). There a good article about it here: https://blog.webdevsimplified.com/2022-05/use-deferred-value/.
So in your case you could replace your optimizedAddOrUpdateNote function with a useEffect hook that have a dependency on the deferredValue. Something like this:
import { useState, useDeferredValue, useEffect } from "react";
export default function App() {
const [note, setNote] = useState("");
const deferredNote = useDeferredValue(note);
useEffect(() => {
console.log("call api with deferred value");
}, [deferredNote]);
function handleNoteChange(e) {
setNote(e.target.value);
}
return (
<>
<input type="text" value={note} onChange={handleNoteChange} />
<p>{note}</p>
</>
);
}
I'm using the YTS API and I need to change the link for the call, I have to use
?query_term= and add the text that the user is typing, for autocomplete. I'm using mantine components for the autocomplete. I tried putting the call inside the handlechange function, but this is not possible.
const [movieNames, setMovieNames] = useState([])
const onChangeHandler = (text) => {
useEffect(() => {
const loadMovieNames = async () => {
const response = await axios.get('https://yts.mx/api/v2/list_movies.json?query_term='+text);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames)
}
loadMovieNames()
}, [])
}
.
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={e => onChangeHandler(e.target.value)}
/>
You MUST use hooks in the execution context of Function Component, you used the useEffect inside a function not in the execution context of Function Component.
const YourComponent = () => {
const [movieNames, setMovieNames] = useState([]);
const loadMovieNames = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach((i) => {
arrayOfMoviesNames.push(i.title);
});
setMovieNames(arrayOfMoviesNames);
};
return (
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(value) => loadMovieNames(value)}
/>
);
};
It is also possible without useEffect, so without making it so complicated by using useEffect and onChangeHandler both, only use onChangeHandler function to update the movieNames and it will automatically update the DOM texts (I mean where ever you use)...
import React, { useState } from "react";
function MoviesPage(props) {
const [ movieNames, setMovieNames ] = useState([]);
const [ searchValue, setSearchValue ] = useState("");
const onChangeHandler = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames);
}
return (
<div>
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(e) => onChangeHandler(e.target.value)}
/>
</div>
);
}
export default MoviesPage;
...and just to clarify, you can use useEffect in case of API if you want to initialize the page with the API data. You can use this hook if you don't have any onChange handlers. Another way you can approach is you can update a state hook (like searchData) on the change of the Search Bar, and lastly add the the searchData variable to the useEffect dependency array:
useEffect(() => {
// use the searchData variable to populate or update the page
// ...
},
[
searchData, // <-- talking about this line
]);
So, this was my solution. Hope this helps you mate!
useEffect is a hook, which executes on state change, So keep the useEffect funtion outside the onChangeHandler and add a new state for 'query param' and setQueryState(text) inside the onChangeHandler, and put the state param as dependency in useEffect, So whenever this state gets changed this will call the use effect function automatically.
I'm having problems with setting up lodash debounce in the function to make an API request. For some reason callback doesn't happen and the value sends every time I type.
import debounce from "lodash/debounce";
const handleChange = (event) => {
const { value } = event.target;
const debouncedSave = debounce((nextValue) => dispatch(movieActions.getMovies(nextValue), 1000));
debouncedSave(value);
};
I'm using material ui and have this in return:
<Autocomplete
onInputChange={handleChange}
/>
Your debounced function is created multiple times for each change event and that causes the problem. I will use a simplified example with a simple input and a console.log instead of your dispatch, but you can apply the solution to your case as well.
The simplest solution would be to move the debouncedSave declaration outside your component.
const debouncedSave = debounce((nextValue) => console.log(nextValue), 1000);
export default function App() {
const handleChange = (e) => {
const { value } = e.target;
debouncedSave(value);
};
return <input onChange={handleChange} />;
}
or else if you want to keep the debounced function declaration inside your component you can use a ref, to create and use the same instance each time, no matter the re-renders:
export default function App() {
const debouncedSaveRef = useRef(
debounce((nextValue) => console.log(nextValue), 1000)
);
const handleChange = (e) => {
const { value } = e.target;
debouncedSaveRef.current(value);
};
return <input onChange={handleChange} />;
}
I wrote a program that takes and displays contacts from an array, and we have an input for searching between contacts, which we type and display the result.
I used if in the search function to check if the searchKeyword changes, remember to do the filter else, it did not change, return contacts and no filter is done
I want to do this control with useEffect and I commented on the part I wrote with useEffect. Please help me to reach the solution of using useEffect. Thank you.
In fact, I want to use useEffect instead of if
I put my code in the link below
https://codesandbox.io/s/simple-child-parent-comp-forked-4qf39?file=/src/App.js:905-913
Issue
In the useEffect hook in your sandbox you aren't actually updating any state.
useEffect(()=>{
const handleFilterContact = () => {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
return () => contacts;
},[searchKeyword]);
You are returning a value from the useEffect hook which is interpreted by React to be a hook cleanup function.
See Cleaning up an effect
Solution
Add state to MainContent to hold filtered contacts array. Pass the filtered state to the Contact component. You can use the same handleFilterContact function to compute the filtered state.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const [filtered, setFiltered] = useState(contacts.slice());
const setValueSearch = (e) => setSearchKeyword(e.target.value);
useEffect(() => {
const handleFilterContact = () => {
if (searchKeyword.length >= 1) {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
} else {
return contacts;
}
};
setFiltered(handleFilterContact());
}, [contacts, searchKeyword]);
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts} filter={filtered} />
</div>
);
};
Suggestion
I would recommend against storing a filtered contacts array in state since it is easily derived from the passed contacts prop and the local searchKeyword state. You can filter inline.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const setValueSearch = (e) => setSearchKeyword(e.target.value);
const filterContact = (contact) => {
if (searchKeyword.length >= 1) {
return contact.fullName
.toLowerCase()
.includes(searchKeyword.toLowerCase());
}
return true;
};
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts.filter(filterContact)} />
</div>
);
};