Hey all trying to use a useState react hook to set a state but it does not work, I gone through the official documentation
Seems like i have followed it correctly but still cannot get the hook to set the state:
const [search, setSearch] = useState('');
const { films } = props;
const matchMovieSearch = (films) => {
return films.forEach(item => {
return item.find(({ title }) => title === search);
});
}
const handleSearch = (e) => {
setSearch(e.target.value);
matchMovieSearch(films);
}
<Form.Control
type="text"
placeholder="Search Film"
onChange={(e) => {handleSearch(e)}}
/>
Search var in useState is allways empty even when i debug and can see that e.target.value has to correct data inputed from the html field
setSearch is an async call, you won't be able to get the search immediately after setting the state.
useEffect is here for rescue.
useEffect(() => {
// your action
}, [search]);
Are you sure you are using the hooks inside a component, hooks can only be used in a Functional React Component.
If that is not the case, there must be something wrong with the Form.Control component, possibly like that component did not implement the onChanged parameter properly.
This is the one I tested with the html input element, and it is working fine. I used the useEffect hook to track the changes on the search variable, and the you can see that the variable is being properly updated.
https://codesandbox.io/s/bitter-browser-c4nrg
export default function App() {
const [search, setSearch] = useState("");
useEffect(() => {
console.log(`search was changed to ${search}`);
}, [search]);
const handleSearch = e => {
setSearch(e.target.value);
};
return (
<input
type="text"
onChange={e => {
handleSearch(e);
}}
/>
);
}
Related
I'm trying to implement debounce for mui autocomplete,
but it's not working well.
I want to send a debounced request to server when inputValue change.
Am I miss something?
It looks loadData fired every input change. Only first load debounce works.
https://codesandbox.io/s/debounce-not-working-j4ixgg?file=/src/App.js
Here's the code from the sandbox:
import { useState, useCallback } from "react";
import { Autocomplete, TextField } from "#mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const loadData = () => {
// sleep(1000)
const filteredOptions = topFilms.filter((f) =>
f.title.includes(inputValue)
);
// This log statement added by Ryan Cogswell to show why it isn't working.
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = useCallback(debounce(loadData, 1000), []);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData();
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}
tldr: codesandbox link
You can accomplish this with a few hooks working together. First lets look at a hook for debouncing state:
function useDebounce(value, delay, initialValue) {
const [state, setState] = useState(initialValue);
useEffect(() => {
console.log("delaying", value);
const timer = setTimeout(() => setState(value), delay);
// clear timeout should the value change while already debouncing
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return state;
}
The above takes in a value and returns the debounced value from the hook later.
The callback at the end of the useEffect prevents a bunch of timers triggering one after the other.
Then your component can be reduced to:
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const debouncedValue = useDebounce(inputValue, 1000);
// fetch data from server
useEffect(() => {
console.log("fetching", debouncedValue);
const filteredOptions = topFilms.filter((f) =>
f.title.includes(debouncedValue)
);
setOptions(filteredOptions);
}, [debouncedValue]);
const handleInputChange = (e, v) => {
setInputValue(v);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
// props
/>
</div>
);
}
The useEffect here is run when the dependency debouncedValue is changed via react state business.
When typing in the TextField your console should look like:
delaying s
delaying sp
delaying spi
fetching spi
delaying spir
fetching spir
This useEffect in App leaves you with a good place to do your fetch to a server like you mentioned you'll need.
The useEffect could easily be replaced with something like useQuery
In your sandbox, loadData was being successfully debounced, however it was always executed with inputValue === "" which then matched all of the options. All of the actual filtering was then being immediately done by the Autocomplete component just the same as if you had provided the full set of options statically. Due to the behavior of JavaScript closures, inputValue is always the empty string because that is its initial value as you create the debounced version of loadData.
Whenever creating debounced functions, I recommend declaring and defining the original function and the debounced version at the top level rather than inside your component. Any local variables and state that your debounced function is dependent on can be passed in as arguments to the function (e.g. in my version of your code, the input value and the setOptions function are passed to debouncedLoadData). This avoids accidentally using stale values due to closure behavior and removes the need for useCallback.
Below is a reworked version of your code that moves loadData and debouncedLoadData to the top level. I also changed loadData to provide no data (rather than all data) when it is passed an empty string so that the debounce behavior is easier to see -- otherwise whenever the Autocomplete has the full set of options, the filtering will occur immediately based on the options that the Autocomplete already has. This also more closely simulates what real code would likely do (i.e. avoid calling the back end for empty string).
import { useState } from "react";
import { Autocomplete, TextField } from "#mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
const loadData = (inputValue, setOptions) => {
if (inputValue.length === 0) {
console.log("no options");
setOptions([]);
return;
}
const filteredOptions = topFilms.filter((f) =>
f.title.toLowerCase().includes(inputValue.toLowerCase())
);
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = debounce(loadData, 1000);
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData(v, setOptions);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}
i am a React newbie (30h learning) and some basic Javascript background. Now i am learning with a course, and tried to "leave" the path. But i am curious how my intended goal could be achieved.
There is a Memegenerator who get all the images of memes from an API in the beginning of the rendering. This is solved with an useEffect-Hook. Now i want that the function getMemeImage() is running ONCE at the beginning AFTER the API-Call was made and the state was updated (this is not part of the course, but i want to try it anyway).
But its giving me an error: "Can't perform a React state update on an unmounted component"
While my research i found that things like didMount(tbh i not understand) and so on are not the "modern" way of react with primarily using hooks and you can simply use an second useEffect. But that is unfortunately not working for me.
How can i solve this and is there an "simple" way for a beginner or it is advanced stuff? I thought maybe to use a timeoutfunction, but seems to be very bad coding.
import React from "react"
export default function Meme() {
const [meme, setMeme] = React.useState({
topText: "",
bottomText: "",
randomImage: ""
})
const [allMemes, setAllMemes] = React.useState([])
React.useEffect(() => { /* first useEffect */
fetch("https://api.imgflip.com/get_memes")
.then(res => res.json())
.then(data => setAllMemes(data.data.memes))
}, [])
React.useEffect(() => { /* This should run after the setAllMemes in the first useEffect was complete */
getMemeImage()
}, [allMemes])
function getMemeImage() {
const randomNumber = Math.floor(Math.random() * allMemes.length)
const url = allMemes[randomNumber].url
setMeme(prevMeme => ({
...prevMeme,
randomImage: url
}))
}
function handleChange(event) {
const {name, value} = event.target
setMeme(prevMeme => ({
...prevMeme,
[name]: value
}))
}
return (
<main>
<div className="form">
<input
type="text"
placeholder="Top text"
className="form--input"
name="topText"
value={meme.topText}
onChange={handleChange}
/>
<input
type="text"
placeholder="Bottom text"
className="form--input"
name="bottomText"
value={meme.bottomText}
onChange={handleChange}
/>
<button
className="form--button"
onClick={getMemeImage}
>
Get a new meme image 🖼
</button>
</div>
<div className="meme">
<img src={meme.randomImage} className="meme--image" />
<h2 className="meme--text top">{meme.topText}</h2>
<h2 className="meme--text bottom">{meme.bottomText}</h2>
</div>
</main>
)
}
First of all, "Can't perform a React state update on an unmounted component" is a warning, not an error.
In order to call 'getMemeImage' only after the first useEffect hook execution, you can check the value of a binary flag that changes after the first useEffect was executed and the response of the async function was recieved.
import React from "react";
export default function Meme() {
const [meme, setMeme] = React.useState({
topText: "",
bottomText: "",
randomImage: ""
});
const [allMemes, setAllMemes] = React.useState([]);
const isInitialRender = React.useRef(true);
React.useEffect(() => {
fetch("https://api.imgflip.com/get_memes")
.then((res) => res.json())
.then((data) => {
isInitialRender.current = false;
setAllMemes(data.data.memes);
});
}, []);
React.useEffect(() => {
/* Check if it is called before the first useEffect data fetching was completed */
if (!isInitialRender.current) getMemeImage();
}, [allMemes]);
function getMemeImage() {
const randomNumber = Math.floor(Math.random() * allMemes.length);
const url = allMemes[randomNumber].url;
setMeme((prevMeme) => ({
...prevMeme,
randomImage: url
}));
}
}
Using the useRef hook persists the value it references between renders.
You can just add control to your second useEffect, it will solve your issue. This is working sample https://codesandbox.io/s/heuristic-matan-3kdbmv
React.useEffect(() => {
if (allMemes.length > 0) {
getMemeImage();
}
}, [allMemes]);
I'm using React Hook Form v7 and I'm trying to make my data form persistent on page reload. I read the official RHF documentation which suggests to use little state machine and I tried to implement it but without success. Is there a better way to do it? However...
The first problem I encountered using it, is that my data is a complex object so the updateAction it should be not that easy.
The second problem is that I don't know when and how to trigger the updateAction to save the data. Should I trigger it on input blur? On input change?
Here's my test code:
If persisting in the localStorage works you, here is how I achieved it.
Define a custom hook to for persisting the data
export const usePersistForm = ({
value,
localStorageKey,
}) => {
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(value));
}, [value, localStorageKey]);
return;
};
Just use it in the form component
const FORM_DATA_KEY = "app_form_local_data";
export const AppForm = ({
initialValues,
handleFormSubmit,
}) => {
// useCallback may not be needed, you can use a function
// This was to improve performance since i was using modals
const getSavedData = useCallback(() => {
let data = localStorage.getItem(FORM_DATA_KEY);
if (data) {
// Parse it to a javaScript object
try {
data = JSON.parse(data);
} catch (err) {
console.log(err);
}
return data;
}
return initialValues;
}, [initialValues]);
const {
handleSubmit,
register,
getValues,
formState: { errors },
} = useForm({ defaultValues: getSavedData() });
const onSubmit: SubmitHandler = (data) => {
// Clear from localStorage on submit
// if this doesn’t work for you, you can use setTimeout
// Better still you can clear on successful submission
localStorage.removeItem(FORM_DATA_KEY);
handleFormSubmit(data);
};
// getValues periodically retrieves the form data
usePersistForm({ value: getValues(), localStorageKey: FORM_DATA_KEY });
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
)
}
I already faced this issue and implemented it by creating a custom Hook called useLocalStorage. But since you are using the React hook form, it makes the code a bit complicated and not much clean!
I suggest you simply use the light package react-hook-form-persist.
The only work you need to do is to add useFormPersist hook after useForm hook. Done!
import { useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";
const yourComponent = () => {
const {
register,
control,
watch,
setValue,
handleSubmit,
reset
} = useForm({
defaultValues: initialValues
});
useFormPersist("form-name", { watch, setValue });
return (
<TextField
title='title'
type="text"
label='label'
{...register("input-field-name")}
/>
...
);
}
The state itself won't persist any data on page reload.
You need to add your state data to Local Storage.
Then load it back into the state on componentDidMount (useEffect with empty dependency array).
const Form = () => {
const [formData, setFormData] = useState({})
useEffect(() => {
if(localStorage) {
const formDataFromLocalStorage = localStorage.getItem('formData');
if(formDataFromLocalStorage) {
const formDataCopy = JSON.parse(formDataFromLocalStorage)
setFormData({...formDataCopy})
}
}
}, []);
useEffect(() => {
localStorage && localStorage.setItem("formData", JSON.stringify(formData))
}, [formData]);
const handleInputsChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return (
<div>
<input
type="text"
name="firstName"
placeholder='first name'
onChange={e => handleInputsChange(e)}
value={formData?.firstName}
/>
<input
type="text"
name="lastName"
placeholder='last name'
onChange={e => handleInputsChange(e)}
value={formData?.lastName}
/>
</div>
)
}
Hi I do have to following simplyfied code. I use Formik as a Validation. Also Material Ui and Reactjs. The Form, Row and Col Tags come from Material. The FastField is same as InputField.
What I want is onClick in the Inputfield a dropdown appears and shows an array which I fetched with the axios-Request.
´´´
const url = 'http://localhost:3000';
const [searchValues, setSearchValues] = useState([]);
const getDropdownItems = (event) => {
console.log('event', event.target.getAttribute('id'));
axios
.get(`${url}/${event.target.getAttribute('id')}`)
.then(
(res) => setSearchValues(res),
console.log('restl', searchValues)
);
};
render(
<Form
onFocus={getDropdownItems}
onSubmit={formik.handleSubmit}
>
<Row>
<Col xs="auto" style={minWidth}>
<FastField
id="DatumEingabe"
name="DatumEingabe"
component={Autocomplete}
label="Datum-Eingabe"
type="text"
options={searchValues}
/>
</Col>
</Row>
</Form>
)
When I check my console I get from the first console.log the name of
the Inputfield. The second console.log says the array is empty,
despite the res is available and should be set. Why does it not work
this way.
setSearchValues(res) will not update searchValues until the next render. If you want to log it each time it changes, you should instead do
const [searchValues, setSearchValues] = useState([]);
useEffect(() => {
console.log(searchValues);
}, [searchValues]);
const getDropdownItems = (event) => {
console.log('event', event.target.getAttribute('id'));
axios
.get(`${url}/${event.target.getAttribute('id')}`)
.then(
(res) => setSearchValues(res)
);
};
I don't think the change is made inmediatly. Try logging searchValues after a second or something like that to see if that is the problem.
const getDropdownItems = (event) => {
console.log('event', event.target.getAttribute('id'));
axios
.get(`${url}/${event.target.getAttribute('id')}`)
.then(
(res) => {
setSearchValues(res);
setTimeout(() => {
console.log('restl', searchValues);
}, 1000)
}
);
};
Also, you have the useEffect hook, which fires an event when a variable is change, so if you want to log it the second it changes you should use:
useEffect(() => {
console.log(searchValues);
}, [searchValues])
To acomplish that, remember to import:
import { useEffect } from "react";
or use
React.useEffect(...)
I am trying to set the search a brand from searchbar and then find it from a prop passed in this component, and for that i thought of using useState which is not being called any time when searbrand is changed. And also, why when i try to setState on onChange or onClick, it never changes immediately. How does it work??
const [searchBrand, setSearchBrand] = useState('');
const [searchBrandHolder, setSearchBrandHolder] = useState('');
const [searching, setSearching] = useState(false)
const brandSearchHandler = (e) => {
}
useState (() => {
if (searchBrandHolder === "") {
setSearching(false);
}
setSearching(true);
console.log("searching state = "+searching);
console.log("brand name searching = "+searchBrand);
}, [searchBrand])
console.log("brand to find = "+searchBrand);
return (
<div>
<div className="searchByBrand">
<input type="text" onChange={e => setSearchBrandHolder(e.target.value)} placeholder="Search by brand"></input>
<SearchIcon className="searchIcon" onClick={e => {setSearchBrand(searchBrandHolder); brandSearchHandler()}} />
what you need is useEffect readmore. Here is how your code should be structured:
useEffect (() => {
if (searchBrandHolder === "") {
setSearching(false);
}
setSearching(true);
console.log("searching state = "+searching);
console.log("brand name searching = "+searchBrand);
}, [searchBrandHolder])