How to debounce a controlled input? - javascript

I'm currently struggling with react inputs and debounce from lodash.
Most of the time when I have a form I also have an edit option, so I need a controlled component to fill back the inputs using value={state["targetValue"]} so I can fill and edit the field.
However, if the component is controlled debounce isn't working.
I made a simple example on CodeSandbox: https://codesandbox.io/embed/icy-cloud-ydzj2?fontsize=14&hidenavigation=1&theme=dark
Code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { debounce } from "lodash";
import "./styles.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: "",
title: "",
editMode: false
};
this.debouncedEvent = React.createRef();
}
debounceEvent(_fn, timer = 500, options = null) {
this.debouncedEvent.current = debounce(_fn, timer, options);
return e => {
e.persist();
return this.debouncedEvent.current(e);
};
}
componentWillUnmount() {
this.debouncedEvent.current.cancel();
}
onChangeValue = event => {
const { name, value } = event.target;
this.setState(() => {
return { [name]: value };
});
};
onRequestEdit = () => {
this.setState({ name: "Abla blabla bla", editMode: true });
};
onCancelEdit = () => {
if (this.state.editMode) this.setState({ name: "", editMode: false });
};
onSubmit = event => {
event.preventDefault();
console.log("Submiting", this.state.name);
};
render() {
const { name, editMode } = this.state;
const isSubmitOrEditLabel = editMode ? `Edit` : "Submit";
console.log("rendering", name);
return (
<div className="App">
<h1> How to debounce controlled input ?</h1>
<button type="button" onClick={this.onRequestEdit}>
Fill with dummy data
</button>
<button type="button" onClick={this.onCancelEdit}>
Cancel Edit Mode
</button>
<div style={{ marginTop: "25px" }}>
<label>
Controlled / Can be used for editing but not with debounce
</label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
value={name}
placeholder="type something"
// onChange={this.onChangeValue}
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
<div style={{ marginTop: "25px" }}>
<label> Uncontrolled / Can't be used for editing </label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
placeholder="type something"
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

At first it looks impossible, but there is a simple way to do it.
Just create the debounce function expression outside the react component.
Here is a pseudo example, for modern React with Hooks:
import React, { useState, useEffect } from "react";
import { debounce } from "lodash";
...
const getSearchResults = debounce((value, dispatch) => {
dispatch(getDataFromAPI(value));
}, 800);
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
useEffect(() => {
getSearchResults(inputValue, dispatch);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
export default SearchData;
Update: In time I came up with a better solution using "useCallback"
import React, { useState, useEffect, useCallback } from "react";
import { debounce } from "lodash";
...
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
const getSearchResults = useCallback(
debounce(value => {
dispatch(getDataFromAPI(value));
}, 800),
[]
);
useEffect(() => {
getSearchResults(inputValue);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};

So... Apparently, there's no solution. the input takes the value from the state. While debounce prevents the state to trigger.
I made a workaround using ReactDOM.
import ReactDOM from "react-dom";
export const setFormDefaultValue = (obj, ref) => {
if (ref && !ref.current) return;
if (!obj || !obj instanceof Object) return;
const _this = [
...ReactDOM.findDOMNode(ref.current).getElementsByClassName("form-control")
];
if (_this.length > 0) {
_this.forEach(el => {
if (el.name in obj) el.value = obj[el.name];
else console.error(`Object value for ${el.name} is missing...`);
});
}
};
and then the use:
this.refForm = React.createRef();
setFormDefaultValue(this.state, refForm)
This way I can fill my form with the state default value and continue using debounce.

Take a look at this lib: https://www.npmjs.com/package/use-debounce
Here's an example of how to use it:
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}

You can try this.
import React, { useState, useCallback, useRef, useEffect } from 'react';
import _ from 'lodash';
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const DeboucnedInput = React.memo(({ value, onChange }) => {
const [localValue, setLocalValue] = useState('');
const prevValue = usePrevious(value);
const ref = useRef();
ref.current = _.debounce(onChange, 500);
useEffect(() => {
if (!_.isNil(value) && prevValue !== value && localValue !== value) {
setLocalValue(value);
}
}, [value]);
const debounceChange = useCallback(
_.debounce(nextValue => {
onChange(nextValue);
}, 1000),
[]
);
const handleSearch = useCallback(
nextValue => {
if (nextValue !== localValue) {
setLocalValue(nextValue);
debounceChange(nextValue);
}
},
[localValue, debounceChange]
);
return (
<input
type="text"
value={localValue}
onChange={handleSearch}
/>
);
});

I had a similar issue, useMemo and debounce from lodash seem to work for me.
import React, { useState, useEffect, useMemo } from 'react';
import { debounce } from 'lodash';
const Search = () => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleFetch = (input) => {
// do stg
};
const debouncedFetch = useMemo(() => debounce(handleFetch, 500), []);
useEffect(() => {
debouncedFetch(inputValue);
}, [inputValue]);
return (
<Form>
<input
type="text"
value={inputValue}
onChange={handleChange}
/>
// rest of the component
</Form>
);
};
Some articles that helped me:
https://dev.to/alexdrocks/using-lodash-debounce-with-react-hooks-for-an-async-data-fetching-input-2p4g
https://www.carlrippon.com/using-lodash-debounce-with-react-and-ts/
https://blog.logrocket.com/how-and-when-to-debounce-or-throttle-in-react/

Related

While rendering a component it is showing an error- "Cannot update a component (`App`) while rendering a different component (`EventList`). "

I Can't render my events. Its showing this error -
"Cannot update a component (App) while rendering a different component (EventList). To locate the bad setState() call inside EventList, follow the stack trace as described in https://reactjs.org/link/setstate-in-render"
Here is EventList Component code -
import { useEffect, useState } from "react";
import EventList from "../../event-list";
import EventForm from "../event-form";
const EventAction = ({
getEventsByClockID,
addEvent,
updateEvent,
clockID,
deleteEvent,
deleteEventsByClockID,
}) => {
const [isCreate, setIsCreate] = useState(false);
const [isToggle, setIsToggle] = useState(false);
const [eventState, setEventState] = useState(null)
const handleCreate = () => {
setIsCreate(!isCreate);
}
useEffect(() => {
setEventState(getEventsByClockID(clockID, true));
}, [isToggle])
const handleToggle = () => {
setIsToggle(!isToggle);
}
return (
<div>
<div>
<button onClick={handleCreate}>Create Event</button>
<button onClick={handleToggle}>Toggle Events</button>
</div>
{isCreate && (
<>
<h3>Create Event</h3>
<EventForm
clockID={clockID}
handleEvent={addEvent}
/>
</>
)}
{isToggle && (
<>
<h3>Events of this clock</h3>
<EventList
clockID={clockID}
eventState={eventState}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</>
)}
</div>
)
}
export default EventAction;
Here is my App Component Code -
import ClockList from "./components/clock-list";
import LocalClock from "./components/local-clock";
import useApp from "./hooks/useApp";
import { localClockInitState } from "./initialStates/clockInitState";
const App = () => {
const {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
} = useApp(localClockInitState);
return (
<div>
<LocalClock
clock={localClock}
updateClock={updateLocalClock}
createClock={createClock}
/>
<ClockList
clocks={clocks}
localClock={localClock.date}
updateClock={updateClock}
deleteClock={deleteClock}
getEventsByClockID={getEventsByClockID}
addEvent={addEvent}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</div>
)
}
export default App;
and Here is my useApp hook -
import { useState } from "react";
import deepClone from "../utils/deepClone";
import generateID from "../utils/generateId";
import useEvents from "./useEvents";
const getID = generateID('clock');
const useApp = (initValue) => {
const [localClock, setLocalClock] = useState(deepClone(initValue));
const [clocks, setClocks] = useState([]);
const {
// events,
// getEvents,
getEventsByClockID,
addEvent,
deleteEvent,
deleteEventsByClockID,
updateEvent,
} = useEvents();
const updateLocalClock = (data) => {
setLocalClock({
...localClock,
...data,
})
}
const createClock = (clock) => {
clock.id = getID.next().value;
setClocks((prev) => ([
...prev, clock
]))
}
const updateClock = (updatedClock) => {
setClocks(clocks.map(clock => {
if(clock.id === updatedClock.id) return updatedClock;
return clock;
}));
}
const deleteClock = (id) => {
setClocks(clocks.filter(clock => clock.id !== id));
}
return {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
}
}
export default useApp;
I want to show all events incorporated with each individual clock.

Update a component after useState value updates

Having a monaco-editor inside a React component:
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
The defaultValue, the default code inside of the editor, is sent via props to the component:
const MyComponent = ({
originalCode
}: MyComponentProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
When the user edits the code, onChange={onChangeCode} is called:
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
My question is, how to reset the code to the original one when the user clicks on Cancel?
Initially it was like:
const handleCancel = () => {
onChangeCode(defaultValue);
};
but it didn't work, probably because useState is asynchronous, any ideas how to fix this?
Here is the whole component for more context:
import Editor from '#monaco-editor/react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, HeaderWithButtons } from '../shared/ui-components';
import { ICalculationEngine } from '../../../lib/constants/types';
import { usePostScript } from '../../../lib/hooks/use-post-script';
import { scriptPayload } from '../../../mocks/scriptPayload';
import { editorDefaultValue } from '../../../utils/utils';
export interface ScriptDefinitionProps {
realInputDetails: Array<ICalculationEngine['RealInputDetails']>;
realOutputDetails: ICalculationEngine['RealInputDetails'];
originalCode: string;
scriptLibId: string;
data: ICalculationEngine['ScriptPayload'];
}
const ScriptDefinition = ({
realInputDetails,
realOutputDetails,
originalCode
}: ScriptDefinitionProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
const [code, setCode] = useState(defaultValue);
const { handleSubmit } = useForm({});
const { mutate: postScript } = usePostScript();
const handleSubmitClick = handleSubmit(() => {
postScript(scriptPayload);
});
const handleCancel = () => {
onChangeCode(defaultValue);
};
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
useEffect(() => {
setDefaultValue(editorDefaultValue(realInputDetails, realOutputDetails));
}, [realInputDetails, realOutputDetails, originalCode]);
return (
<div>
<HeaderWithButtons>
<div>
<Button title='cancel' onClick={handleCancel} />
<Button title='save' onClick={handleSubmitClick} />
</div>
</HeaderWithButtons>
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
</div>
);
};
export default ScriptDefinition;
If you need the ability to change the value externally, you'll need to use the Editor as a controlled component by passing the value prop (sandbox):
For example:
const defaultValue = "// let's write some broken code 😈";
function App() {
const [value, setValue] = useState(defaultValue);
const handleCancel = () => {
setValue(defaultValue);
};
return (
<>
<button title="cancel" onClick={handleCancel}>
Cancel
</button>
<Editor
value={value}
onChange={setValue}
height="90vh"
defaultLanguage="javascript"
/>
</>
);
}

How to search drinks based on a checkbox value

I created a search component to get the cocktails by name, but I want to add another search option based on a checkbox(so the cocktail is alcoholically or not).
I have a context.js file:
import React, { useState, useContext, useEffect } from 'react'
import { useCallback } from 'react'
const url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s='
const AppContext = React.createContext()
const AppProvider = ({ children }) => {
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('a')
const [searchCheckbox, setSearchCheckbox] = useState(false)
const [cocktails, setCocktails] = useState([])
const fetchDrinks = useCallback(async () => {
setLoading(true)
setSearchCheckbox(false)
try {
const response = await fetch(`${url}${searchTerm}`)
const data = await response.json()
const {drinks} = data
if(!drinks) {
setCocktails([])
} else {
const searchedCocktails = drinks.map((drink) => {
const {idDrink, strDrink, strDrinkThumb, strInstructions,strAlcoholic, strIngredient1,strIngredient2} = drink
return {
id: idDrink,
name: strDrink,
image: strDrinkThumb,
isAlcoholic: strAlcoholic,
info: strInstructions,
ingredient1: strIngredient1,
ingredient2: strIngredient2
}
})
setCocktails(searchedCocktails)
}
setLoading(false)
} catch (error) {
console.log(error)
setLoading(false)
}
}, [searchTerm])
useEffect(() => {
fetchDrinks()
}, [searchTerm, fetchDrinks])
return <AppContext.Provider
value={{loading,
cocktails,
setSearchTerm,
setSearchCheckbox
}}>
{children}
</AppContext.Provider>
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
The searchbar component is the following:
import React from 'react'
import { useGlobalContext } from '../helpers/context'
export default function SearchBar() {
const searchValue = React.useRef('')
const searchCheckbox = React.useRef(false)
const {setSearchTerm} = useGlobalContext()
const {setSearchCheckbox} = useGlobalContext()
const searchCocktail = () => {
setSearchTerm(searchValue.current.value)
setSearchCheckbox(searchCheckbox.current.checked)
}
const handleSubmit = (e) =>{
e.preventDefault()
}
//setup auto focus on input
React.useEffect(() => {
searchValue.current.focus()
searchCheckbox.current.focus()
}, [])
return (
<div className="container">
<div className="row">
<div className="col-12">
<div className="input-group">
<input className="form-control border-secondary py-2" type="search" ref={searchValue} onChange={searchCocktail}/>
<div className="input-group-append">
<button onClick={handleSubmit} className="btn btn-outline-secondary" type="button">
<i className="fa fa-search"></i>
</button>
</div>
</div>
</div>
<div className="col-12">
<div className="form-check">
<input className="form-check-input" type="checkbox" ref={searchCheckbox} onChange={searchCocktail} id="flexCheckDefault"/>
<label onClick={handleSubmit} className="form-check-label" htmlFor="flexCheckDefault">
Alcoholic
</label>
</div>
</div>
</div>
</div>
)
}
Can somebody can path me to a way to solve my problem? I haven't coded in React for some time and I think I m doing something hugely wrong with the useState hook on the checkbox
You need to create reference to state in parent element and pass it to child.
const [ alcoholicFilter, setAlcoholicFilter ] = useState(false) // false is default value
// here you can use `alcoholicFilter` variable and it will be updated
...
<Searchbar filter={setAlcoholicFilter}/>
After that in Searchbar component you can use this reference to update parent state
export default (props) => {
...
const handleSubmit = () => {
...
props.filter(input.checked) // you can set here to whatever you need
}
...
}

Using React useRef Hook

I have been building Select Component of my own using button components in React. I am stuck on how to close the menu when the user clicks outside the menu. The code sandbox can be found here:
I am thinking of adding an event listener when the component is mounted, which would track whether the click was made inside of the menu or not. I am expecting to solve this using useRef Hook.
Try this:
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const options = [
{
label: "Apple",
value: "apple"
},
{
label: "Ball",
value: "ball"
},
{
label: "Car",
value: "car"
}
];
return (
<div className="App">
<SelectComponent options={options} />
</div>
);
}
function SelectComponent(props) {
const ref = useRef(null)
const [selectedVal, setSelectedVal] = useState("");
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
document.addEventListener("click", (e) => {
//Insert code here.
});
}, []);
useEffect(() => {
document.addEventListener('click', handleClickOutside, false)
return () => document.removeEventListener('click', handleClickOutside, false)
})
const handleClickOutside = (e) => {
if (ref.current && !ref.current.contains(e.target)) setIsMenuOpen(false)
}
const onSelectClick = () => {
setIsMenuOpen((prevState) => !prevState);
};
const onValueSelect = (e) => {
const {
target: { id }
} = e;
setSelectedVal(id);
setIsMenuOpen((prevState) => !prevState);
};
const computeClass = isMenuOpen ? "close-icon" : "open-icon";
return (
<div {...{ref}}>
<button
className={`select-btn selected-val ${computeClass}`}
onClick={onSelectClick}
>
{selectedVal}
</button>
{isMenuOpen &&
props.options.map((item) => {
return (
<button
key={item.value}
id={item.label}
value={item.value}
className={"select-btn"}
onClick={onValueSelect}
>
{item.label}
</button>
);
})}
</div>
);
}
Simple Way to handle the useRef in the component is shown below :-
Here our target is to resize the input filed on submit button click.
import React, { useState, useRef } from "react";
const UseRef = () => {
const inputRef = useRef();
const [name, setName] = useState("");
const handleSize = ()=>{
console.log("submit");
inputRef.current.style.width="300px"
}
return (
<div>
<label>Name:</label>
<input
ref={inputRef}
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ padding: "10px" }}
/>
<br />
<button onClick={handleSize} >submit</button>
</div>
);
};
export default UseRef;
inputRef is the instance of the useRef which I have passed in the specific DOM element which we are going to manipulate on click event of submit button.
inputRef.current.style.width="300px" this is the manipulation done with the DOM element which is passed in the function handleSize()
When onClcik event is invoked from submit button size of the button will be increased to 300px .
We can also get the same result by avoiding import of useState & by eleminating onChange & value props from the input filed.

try to end up search filter with react but there is mistake

This is my main component for seraching:
import React, {useState, useEffect} from 'react';
import SearchBar from "./SearchBar";
import SearchList from "./SongList";
const SearchMusic = (props) => {
const [input, setInput] = useState('');
const [songListDefault, setSongListDefault] = useState();
const [songList, setSongList] = useState();
const fetchData = async () => {
return await fetch('http://ws.audioscrobbler.com/2.0/?method=tag.gettoptracks&tag=disco&api_key=c17b1886d9465542a9cd32437c804db6&format=json')
.then(response => response.json())
.then(data => {
setSongList(data)
setSongListDefault(data)
});
}
const updateInput = async (input) => {
const filtered = songListDefault.filter(song => {
return song.name.toLowerCase().includes(input.toLowerCase())
})
setInput(input);
setSongList(filtered);
}
useEffect(() => {
fetchData()
}, [])
return (
<div>
<h1>Song List</h1>
<SearchBar
input={input}
onChange={updateInput}
/>
<SearchList songList={songList}/>
</div>
);
};
export default SearchMusic;
below is separate input js file:
import React from 'react';
const SearchMusic = ({keyword, setKeyword}) => {
const BarStyling = {width: "20rem", background: "#F2F1F9", border: "none", padding: "0.5rem"};
return (
<input
type="text"
style={BarStyling}
key='random1'
value={keyword}
placeholder={'Search a song'}
onChange={(e => setKeyword(e.target.value))}
/>
);
};
export default SearchMusic;
end it is my song list below:
import React from 'react';
const SongList = ({songList = []}) => {
return (
<div>
{
songList && songList.tracks.track.map((song, index) => {
if (song) {
return (
<div key={song.name}>
<h1>{song.name}</h1>
</div>
)
}
return null;
}
)
}
</div>
);
};
export default SongList;
I get this mistake --> TypeError: setKeyword is not a function. I don't what's wrong and don't know how to get rid of it. It seems to me problem is in updateInput function more precisely in what it returns --> song.name.toLowerCase(). There is api link:
http://ws.audioscrobbler.com/2.0/?method=tag.gettoptracks&tag=disco&api_key=c17b1886d9465542a9cd32437c804db6&format=json
I need to get name of a song in search input... But something's wrong
The props that this component are expecting are not getting passed into component in your parent component
const SearchMusic = ({keyword, setKeyword}) => {
const BarStyling = {width: "20rem", background: "#F2F1F9", border: "none", padding: "0.5rem"};
return (
<input
type="text"
style={BarStyling}
key='random1'
value={keyword}
placeholder={'Search a song'}
onChange={(e => setKeyword(e.target.value))}
/>
);
};
That is assuming that the following is the above component
<SearchBar
input={input}
onChange={updateInput}
/>
How about
<SearchBar
keyword={input}
setKeyword={updateInput}
/>

Categories

Resources