Using comparison in useEffect's dependency array - javascript

I'm creating a search input with a submit button and a clear button. When submit button is clicked, it will trigger a search request with available input value. When clear button is clicked, it will also trigger a search request with empty value. I try to use comparison in useEffect dependency array to trigger effect when the search value is empty to accomodate the clear button.
const Test = ({ doGetData }) => {
const [status, setStatus] = useState(null);
const [activePage, setActivePage] = useState(1);
const [term, setTerm] = useState("");
const handleFilter = () => {
const query = {
page: activePage,
q: term,
active: status
};
doGetData(query);
};
useEffect(() => {
handleFilter();
}, [status, activePage, term === ""]);
const setEmptySearch = () => setTerm("");
const handleInputChange = e => {
const { value } = e.currentTarget;
setTerm(value);
};
return (
<SearchInput
handleFilter={handleFilter}
handleDismissSearch={setEmptySearch}
handleInputChange={handleInputChange}
status={status}
term={term}
/>
);
};
It worked well when I click search button, click the clear button, and also when I manually erase the input.But, the only problem I have is when I type the first letter, it will trigger the search. Can you help me guys?

Just check with an if-statement inside of your useEffect-function:
useEffect(() => {
if(term === "") {
handleFilter();
}
}, [status, activePage, term]);

Related

How can we handle disabling of dropdown selection in react hooks web app without refreshing the page?

How can we handle disabling of dropdown selection in react hooks web app without refreshing the page ? In my case, I have a multiselect dropdown box. When I selected an item from the dropdown, display a text field, once after typing some text and submit it, details get saved into database. Once saved into DB, the respective dropdown item should be disabled for selection.
But in my case, its not immediately disabled after submit. Its is disabled only after i manually refresh the page. How can I fix this issue, can someone please advise ?
const [option, setOption] = useState([]);
const [selectedOption, setSelectedOption] = useState([]);
const {
register,
handleSubmit,
watch,
formState: { errors },
reset,
} = useForm();
const refSelect = useRef(null);
const [submittedNominees, setSubmittedNominees] = useState([{}]);
const [maxOptions, setMaxOptions] = useState(0);
const [showOptions, setShowOptions] = useState(false);
const focusOnInput = () => {
setTimeout(() => {
document.querySelector("input").focus();
// Adding some delay to allow the component to re-mount
}, 10);
};
const handleTypeSelect = (e, i) => {
const copy = [...selectedOption];
copy.push(e[3 - maxOptions]); //A.H-fix error: select one more record it still console log the pre selected one
setSelectedOption(copy);
setMaxOptions((prevState) => prevState - 1); //A.H-making maxOption dynamic
focusOnInput();
};
const handleTypeRemove = (e) => {
const copy = [...selectedOption];
let index = copy.indexOf(e);
copy.splice(index, 1);
setSelectedOption(copy);
setMaxOptions((prevState) => prevState + 1);
// immutating state (best practice)
const updateList = nomRegister.map((item) => {
return { ...item };
});
//delete the specific array case depends on the id
updateList.splice(index, 1);
setNomRegister(updateList);
focusOnInput();
};
useEffect(() => {
const fetchData = async () => {
const userEmail = localStorage.getItem("loginEmail");
try {
let res = [];
res = await Axios.get(
`${appURL}/service/submittednominations`,
{params:{userEmail}}
);
const data1 = res.data;
console.log(data1, "data1");
setSubmittedNominees(data1);
setMaxOptions(3 - data1.length); //maxOption dynamic because we don't the length of data from submittednominations
console.log("Submitted nominations :" + JSON.stringify(data1));
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
Droddown box:
<section className="col1">
<div className='nomineeSelectBox'>
<div id='dialog2' className='triangle_down1' />
<div className='arrowdown'>
<Multiselect
ref={refSelect}
onSelect={(e) => handleTypeSelect(e, selectedOption.length)}
onRemove={handleTypeRemove}
options={!showOptions ? [] : option}
displayValue='displayValue'
disablePreSelectedValues={true}
selectedValues={submittedNominees}
showCheckbox={true}
emptyRecordMsg={"Maximum nominees selected !"}
/>
</div>
</div>
</section>
If you add the changed state to the dependency list of the useEffect(), it will re-run that section of code inside.
useEffect(() => {
// this code will re-run every time "selectedOptions" is changed
// therefore every time "handleTypeSelect()" or "handleRemove()" is run
const fetchData = async () => {
...
}, [selectedOption])
This will trigger a fresh list of submittedNominees, and cause a rerender (just the component, not the whole page) because submittedNominees is one of the render properties
<Multiselect
...
selectedValues={submittedNominees}
...
/>
Alternatively (and probably quicker UI), make a call to setSubmittedNominees() inside handleTypeSelect() and handleRemove(). That will also trigger a component rerender.

event.target.value omitting last character

i've been solving this problem without any progress for the pas 2 hours or so, here is code:
export const useFetchAll = () => {
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value);
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
console.log(searchItem);
};
useEffect(() => {
const searchRepo = async () => {
setLoading(true);
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
if (searchItem) searchRepo();
}, [searchItem]);
the problem is that when i enter characters in input and set state to event.target.value it doesn't pick up last character. here is an image:
enter image description here
BTW this is a custom hook, i return the onchange function here:
const HomePage = () => {
const { searchResult, loading, searchItem, handleChange, listToDisplay } =
useFetchAll();
and then pass it as a prop to a component like so:
<Stack spacing={2}>
<Search searchItem={searchItem} handleChange={handleChange} />
</Stack>
</Container>
any help? thanks in advance.
You are handling the searchItem and searchResult state variables as if their state change was synchronous (via setSearchItem and setSearchResult) but it isn't! React state setters are asynchronous.
The useEffect callback has a dependency on the searchItem state variable. Now every time the user types something, the state will change, that change will trigger a re-rendering of the Component and after that render finishes, the side-effect (the useEffect callback) will be executed due to the Components' lifecycle.
In our case, we don't want to initiate the fetch request on the next render, but right at the moment that the user enters something on the search input field, that is when the handleChange gets triggered.
In order to make the code work as expected, we need some a more structural refactoring.
You can get rid of the useEffect and handle the flow through the handleChange method:
export const useFetchAll = () => {
const [ loading, setLoading ] = useState( false );
const [ searchItem, setSearchItem ] = useState( "" );
const [ listToDisplay, setListToDisplay ] = useState( [] );
const handleChange = async ( e ) => {
const { value } = e.target;
// Return early if the input is an empty string:
setSearchItem( value );
if ( value === "" ) {
return setListToDisplay( [] );
}
setLoading( true );
const { data } = await axios.get( "https://api.github.com/repositories" );
setLoading( false );
const valueLowercase = value.toLowerCase(); // Tiny optimization so that we don't run the toLowerCase operation on each iteration of the filter process below
setListToDisplay(
data.filter(({ name }) => name.toLowerCase().includes(valueLowercase))
);
};
return {
searchItem,
handleChange,
loading,
listToDisplay,
};
};
function used for updating state value is asynchronous that why your state variable is showing previous value and not the updated value.
I have made some change you can try running the below code .
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value); // this sets value asyncronously
console.log("e.target.value :" + e.target.value); // event.target.value does not omitting last character
console.log("searchItem :" + searchItem); // if we check the value then it is not set. it will update asyncronously
};
const setList = async () => {
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
};
const searchRepo = async () => {
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
// this useeffect execute its call back when searchItem changes a
useEffect(() => {
setList(); // called here to use previous value stored in 'searchResult' and display something ( uncomment it if you want to display only updated value )
if (searchItem) searchRepo();
}, [searchItem]);
// this useeffect execute when axios set fetched data in 'searchResult'
useEffect(() => {
setList();
}, [searchResult]);
// this useeffect execute when data is updated in 'listToDisplay'
useEffect(() => {
console.log("filtered Data") // final 'listToDisplay' will be availble here
console.log(listToDisplay)
}, [listToDisplay]);

Disabling the continue button when there is no selection from the table

I have a list from backend. This list is displaying the user's invoices. I want the button to be disabled if the user doesn't select at least one invoice. If the user selects at least 1 invoice, I want the button to be active. But the invoices and the button are in different components.
payment-table.js
Here the IDE warns me like this; 'setEnabledButton' is assigned a value but never used.
const [selectedItems, setSelectedItems] = useState([]);
const [enabledButton, setEnabledButton] = useState();
useEffect(() => {
if (selectedItems.length > 0) {
setEnabledButton = true;
} else {
setEnabledButton = false;
}
}, [selectedItems]);
policy-payment-info.js
<div className={hiddenControlButtonClass}>
<AS.Button variant="outlined" onClick={handlePayment} enabledButton>
{t('policy-payment-info.continue')}
</AS.Button>
</div>
First set default state for your useState function :
const [enabledButton, setEnabledButton] = useState(false);
After this you need to execute the seState by add ()
Like this :
useEffect(() => {
if (selectedItems.length > 0) {
setEnabledButton(true);
} else {
setEnabledButton(false);
}
}, [selectedItems]);
And then, you need to use this state to enable or disable the button :
<div className={hiddenControlButtonClass}>
<AS.Button variant="outlined" onClick={handlePayment} disabled={enabledButton}>
{t('policy-payment-info.continue')}
</AS.Button>
</div>
Your syntax is a little bit off. setEnabledButton is a function that has to be invoked and given the a certain value. Try this:
const [selectedItems, setSelectedItems] = useState([]);
const [enabledButton, setEnabledButton] = useState(false);
useEffect(() => {
if (selectedItems.length > 0) {
setEnabledButton(true);
} else {
setEnabledButton(false);
}
}, [selectedItems]);

Keeping state of input breaks app when value is undefined

I am trying to get a form to work, which acts both as a creation- and an update-form for an item. I keep track of an edit state to decide what text should be rendered in the form and what API call to make on submitting it. So far so good, that all works well. In order to keep track of the input values, I use a hook useInputState to update the corresponding state value:
useInputState.js
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (e) => {
if (e.target) {
setValue(e.target.value);
} else { // to be able to set state manually
setValue(e);
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
const [newTitle, setNewTitle, resetNewTitle] = useInputState();
form.js
export default function Form({
newTitle,
setNewTitle,
edit, // true or false
}) {
return (
<div>
<h2>{edit ? "Edit Item" : "Add new item"}</h2>
<div>
<label>Title</label>
<input
type="text"
placeholder="What is your title?"
value={newTitle}
onChange={setNewTitle}
/>
</div>
)
}
Now when the user adds a new item, obviously the form is empty to begin with. However, when the user wants to edit an item, I try to prepopulate the form with the information of the given object. This is where I run into issues: The object model has some optional fields. When the user leaves these blank when creating the object, editing it breaks the application with an error of can't read property target of null.
I use the following code to prepoulate the form fields when edit is toggled on:
useEffect(() => {
if (edit && selectedItem) {
setNewTitle(selectedItem.title);
}
}, [edit]);
I see why it's running into issues and I tried a lot of things like changing the setNewTitle argument to setNewTitle(selectedItem["title"] !== undefined ? selectedItem.title : ""); and similar approaches but nothing has worked so far.
What can I do to solve this issue?
It seems that you are only comparing selectedItem.title !== undefined which can result in a specified error if your selectedItem.title is equal to null;
There are multiple ways to solve the issue but you can do the following:
const handleChange = (e) => {
if (!!e && e.target) {
setValue(e.target.value);
} else { // to be able to set state manually
setValue(e);
}
};
The change is in the line: if (!!e && e.target)
In your useInputState hook, you assume that handleChange function is a React.ChangeEvent meaning it is passed with e : Event from which you read e.target.value. This breaks when you try to set the value directly here in this line, in useEffect:
setNewTitle(selectedItem.title);
I would suggest a change like this to your useInputState hook,
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (value) => {
if (value) {
setValue(value);
} else {
setValue("");
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
then i your form do this,
export default function Form({
newTitle,
setNewTitle,
edit, // true or false
}) {
return (
<div>
<h2>{edit ? "Edit Item" : "Add new item"}</h2>
<div>
<label>Title</label>
<input
type="text"
placeholder="What is your title?"
value={newTitle}
onChange={(e)=>setNewTitle(e.target.value)}
/>
</div>
)
}
It should work as you expect it to work with this change.
Your error is can't read property target of null.
So the e arg is falsy sometimes.
Try
setNewTitle(selectedItem.title ? selectedItem.title : '')
and in the custom hook:
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (e) => {
if(typeof e === 'string') {
setValue(e);
} else if(e && e.target) {
setValue(e.target.value);
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};

React how to run a function only once, after page enter or refresh

I'm working on a search function with algolia search.
The user may put in a search term from a different site, get redirected
to the search page, and the search should be invoked.
After this, to search for more stuff, the user need to use the search bar in the
search page, and hit enter to invoke the search again.
I've tried to use a useState and set it to true after the first render.
However when I type in my search bar, the useEffect gets invoked at every key press.
Why is this happening when firstRender doesn't change? It will set firstRender to true again, but since firstRender already is true, nothing should happen?
How can I "deactivate" the useEffect after the first render, so that the user
only uses the search bar to search?
(I don't want to hammer the API with requests while the user is typing)
Thanks!
const SearchRecipes = () => {
const client = algoliasearch(process.env.REACT_APP_ALGOLIA_APP_ID, process.env.REACT_APP_ALGOLIA_SEARCH_API);
const index = client.initIndex('recipe_search');
const match = useRouteMatch();
const [searchTerm, setSearchTerm] = useState(match.params.id);
const [searchReturnData, setSearchReturnData] = useState([]);
const [firstRender, setFirstRender] = useState(false);
const useMountEffect = (fun) => useEffect(fun, [])
useEffect(() => {
handleSubmit();
setFirstRender(true);
}, [firstRender])
const handleSubmit = (e = null) => {
if (e !== null){
e.preventDefault();
}
index
.search(searchTerm)
.then((responses) => {
setSearchReturnData(responses.hits);
});
}
return (
// irrelevant JSX
<div className="keywords">
<input
type="text"
value={searchTerm}
onChange={({target}) => setSearchTerm(target.value)}
/>
</div>
// more irrelevant JSX
)
}
The reason is that your useEffect has a dependency on firstRender and inside it it is setting that value.
You could either follow the suggestion from the comment, using an empty array of dependencies:
useEffect(() => {
handleSubmit();
}, [])
or check if your firstRender state is already set:
useEffect(() => {
if (!firstRender) {
handleSubmit();
setFirstRender(true);
}
}, [firstRender])
This is a really nice article about useEffect if you need more info.

Categories