I hate to upload a code snippet with no sandbox, but this particular instance I use firebase so wasn't sure how to make one. Apologies for the verbose code. I'm a beginner React developer and I've been stuck on this state management issue for 2 weeks now, and I tried so many different methods but to no fruit. Please help.
My goal is to click AddLinkButton to make multiple input forms one by one, each input form would be different links, and by clicking Apply Button it would collect all the link values and store it to firebase's firestore. Once the storing is complete, it would display a preview by passing in multiple updated hook values to <UserPreview />.
If I run this particular code below, the key which is supposed to be the value of the link input forms, is null and does not update on onChange.
Please help... much appreciated. Thank you.
EDIT: changed variable name key to keyHook but to no success. Same issue
const AdminCustomizer = () => {
const [username, setUsername] = useState(null);
const [linkForm, setlinkForm] = useState([]);
const [spotlightLabel, setSpotlightLabel] = useState('');
const [spotlightLink, setSpotlightLink] = useState('');
const [refresh, setRefresh] = useState(false);
const [keyHook, setKeyHook] = useState(null);
const [startCollect, setStartCollect] = useState(false);
const linkRef = useRef();
const userInfo = {username, linkRef, spotlightLabel, spotlightLink, pfpURL, refresh};
// on initial load, load database to page
if (!username) {
firebase.getAuth().onAuthStateChanged(user => {
if (user) {
setUsername(user.displayName);
firebase.getUserInfo(user.displayName).then(result => {
setSpotlightLabel(result.spotlightLabel);
setSpotlightLink(result.spotlightLink);
linkRef.current = result.links;
if (result.links) {
Object.values(result.links).forEach(link => {
AddLinks(link);
});
}
})
}
});
}
//on refresh (when clicking apply changes button) reload page values with updated database
useEffect(() => {
if (refresh) {
firebase.getAuth().onAuthStateChanged(user => {
if (user) {
firebase.getUserInfo(user.displayName).then(result => {
linkRef.current = result.links;
Object.values(result.links).forEach(link => {
AddLinks(link);
});
})
setRefresh(false);
}
});
}
}, [refresh])
// adding AddLink button will add a new input form
// adding AddLink with firebase database value will add a new input form with values loaded
const AddLinks = url => {
const hooks = { refresh, startCollect, keyHook, setKeyHook };
if (url) setKeyHook(url);
setlinkForm([ ...linkForm, <AddLink key={keyHook} keyHook={keyHook} hooks={hooks} /> ]);
}
// add link input form
const AddLink = props => {
const handleChange = e => setKeyHook(e.target.value);
return (
<form noValidate autoComplete="off">
<br />
<Link label="Social" onChange={handleChange} value={props.keyHook} />
</form>
)
}
// when apply changes is clicked, collect input values from all link input forms
if (startCollect) {
linkForm.forEach(form => {
linkRef.current = {
...linkRef.current,
link: form.keyHook,
}
});
firebase.addLinksToUser({ spotlightLabel, spotlightLink, linkRef }).then(() => {
//force refresh to update userInfo for UserPreview
setStartCollect(false);
setRefresh(true);
});
}
return (
<>
<LinkBox>
<ApplyButton onClick={() => setStartCollect(true)}>Apply Changes</ApplyButton>
<Link label="Website Title" onChange={e => setSpotlightLabel(e.target.value)} value={spotlightLabel} />
<Link label="Website URL" onChange={e => setSpotlightLink(e.target.value)} value={spotlightLink}/>
<AddLinkButton onClick={() => AddLinks(null)} />
<div>{linkForm ? linkForm.map(child => child) : null}</div>
</LinkBox>
<div>
<PhoneOutline>
<UserPreview userInfo={userInfo}/>
</PhoneOutline>
</div>
</>
);
}
export default AdminCustomizer;
In AddLink, the key is a restricted keyword and doesn't get propagated as props. Try a different prop name instead of key.
See this link
Try:
<AddLink key={keyHook} keyHook={keyHook} hooks={hooks} />
Related
I'm trying to setup a form. It has Edit feature where on edit I call an API and get the data into state.
I'm struggling to display data in the form after api call. There's no problem utilizing the API or calling the redux functions. Problem is that my Form only displays last data in the redux state but not the updated data.
That's how I'm doing the stuff.
Calling API if isEdit===True at the same time Form is being displayed on component mount.
Updateding state after success as an object called customer
accessing the customer object like this
const { customer } = useSelector((state) => state.customers)
Lets say I have a input field where I want to display the email of customer.
I'm handling this think like that:
email: isEdit ? customer?.email : '', // At this point there is some problem
It loads the previous data that was stored in the state.customer but not the new one.
I believe my email field is rendering first and then doesn't updated the value when change happens in state.customer.
So how I can fix this? So that email value should be changed at the same time if state.customer got changed
Here is the full component. Still removed irrelevant part.
const CustomerNewEditForm = ({ isEdit, id, currentUser}) => {
const dispatch = useDispatch()
const navigate = useNavigate()
console.log('isEdit', isEdit, 'id', id, 'currentUser', currentUser)
// get sales reps
const { customer } = useSelector((state) => state.customers)
// const customer = () => {
// return isEdit ? useSelector((state) => state.customers?.customer) : null
// }
const { enqueueSnackbar } = useSnackbar()
const defaultValues = useMemo(
() => ({
email: isEdit ? customer?.email : '',
name: isEdit ? customer?.name : '',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentUser]
)
const methods = useForm({
resolver: yupResolver(NewUserSchema),
defaultValues
})
const {
reset,
watch,
control,
setValue,
handleSubmit,
formState: { isSubmitting }
} = methods
const values = watch()
useEffect(() => {
if (isEdit === true) {
dispatch(getCustomerDetails(id))
console.log(customer)
}
if (isEdit && currentUser) {
reset(defaultValues)
}
if (!isEdit) {
reset(defaultValues)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEdit, currentUser])
const onSubmit = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 500))
reset()
let body = {
email: values.email,
name: values.name,
}
console.log(body)
dispatch(createCustomer(body))
enqueueSnackbar(!isEdit ? 'Create success!' : 'Update success!')
// navigate(PATH_DASHBOARD.admin.root)
} catch (error) {
console.error(error)
}
}
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Grid item md={3}>
{' '}
<RHFTextField name="name" label="Customer Name" />
</Grid>
<Grid item md={3}>
{' '}
<RHFTextField name="email" label="Email Address" />
</Grid>
</FormProvider>
)
}
export default CustomerNewEditForm
Here in the component defaultValues carries the previous data from customer object if its True and renders the form with those values. but new data comes a miliseconds later but form renders first.
First of all try to console.log your customer data and make sure that it gets a fresh data on last render.
If it gets fresh data, try take a look at your Input component, it might set some initial data, so the input will be editable and controlled by some state.
Try to modify your input's state on redux store update in useEffect.
Currently that's all that I can suggest, update your post with code with your form and input, also post your console.log result, if my answer doesn't helped you.
If the problem would be not in form\input state and console.log wouldn't show you actual updated data in last render, then I will need to see your redux store code to resolve this issue.
Hope it helped
I have a page wherein there are Listings.
A user can check items from this list.
Whenever the user checks something it gets added to a globally declared Set(each item's unique ID is added into this set). The ID's in this set need to be accessed by a seperate Component(lets call it PROCESS_COMPONENT) which processes the particular Listings whose ID's are present in the set.
My Listings code roughly looks like:
import React from "react";
import { CheckBox, PROCESS_COMPONENT } from "./Process.jsx";
const ListItem = ({lItem}) => {
return (
<>
//name,image,info,etc.
<CheckBox lId={lItem.id}/>
</>
)
};
function Listings() {
// some declarations blah blah..
return (
<>
<PROCESS_COMPONENT /> // Its a sticky window that shows up on top of the Listings.
//..some divs and headings
dataArray.map(item => { return <ListItem lItem={item} /> }) // Generates the list also containing the checkboxes
</>
)
}
And the Checkbox and the PROCESS_COMPONENT functionality is present in a seperate file(Process.jsx).
It looks roughly like:
import React, { useEffect, useState } from "react";
let ProcessSet = new Set(); // The globally declared set.
const CheckBox = ({lID}) => {
const [isTicked, setTicked] = useState(false);
const onTick = () => setTicked(!isTicked);
useEffect( () => {
if(isTicked) {
ProcessSet.add(lID);
}
else {
ProcessSet.delete(lID);
}
console.log(ProcessSet); // Checking for changes in set.
}, [isTicked]);
return (
<div onClick={onTick}>
//some content
</div>
)
}
const PROCESS_COMPONENT = () => {
const [len, setLen] = useState(ProcessSet.size);
useEffect( () => {
setLen(ProcessSet.size);
}, [ProcessSet]); // This change is never being picked up.
return (
<div>
<h6> {len} items checked </h6>
</div>
)
}
export { CheckBox, PROCESS_COMPONENT };
The Set itself does get the correct ID values from the Checkbox. But the PROCESS_COMPONENT does not seem to be picking up the changes in the Set and len shows 0(initial size of the set).
I am pretty new to react. However any help is appreciated.
Edit:
Based on #jdkramhoft
's answer I made the set into a state variable in Listings function.
const ListItem = ({lItem,set,setPSet}) => {
//...
<CheckBox lID={lItem.id} pset={set} setPSet={setPSet} />
)
}
function Listings() {
const [processSet, setPSet] = useState(new Set());
//....
<PROCESS_COMPONENT set={processSet} />
dataArray.map(item => {
return <ListItem lItem={item} set={processSet} setPSet={setPSet} />
})
}
And corresponding changes in Process.jsx
const CheckBox = ({lID,pset,setPSet}) => {
//...
if (isTicked) {
setPSet(pset.add(lID));
}
else {
setPSet(pset.delete(lID));
}
//...
}
const PROCESS_COMPONENT = ({set}) => {
//...
setLen(set.size);
//...
}
Now whenever I click the check box I get an error:
TypeError: pset.add is not a function. (In 'pset.add(lID)', 'pset.add' is undefined)
Similar error occurs for the delete function as well.
First of all, the set should be a react state const [mySet, setMySet] = useState(new Set()); if you want react to properly re-render with detected changes. If you need the set to be available to multiple components you can pass it to them with props or use a context.
Secondly, React checks if dependencies like [ProcessSet] has been changed with something like ===. Even though the items in the set are different, no change is detected because the object is the same and there is no re-render.
Update:
The setState portion of [state, setState] = useState([]); is not intended to mutate the previous state - only to provide the next state. So to update your set you would do something like:
const [set, setSet] = useState(new Set())
const itemToAdd = ' ', itemToRemove = ' ';
setSet(prev => new Set([...prev, itemToAdd]));
setSet(prev => new Set([...prev].filter(item => item !== itemToRemove)));
As you might notice, this makes adding and removing from a set as slow as a list. So unless you need to make a lot of checks with set.has() I'd recommend using a list:
const [items, setItems] = useState([])
const itemToAdd = ' ', itemToRemove = ' ';
setItems(prev => [...prev, itemToAdd]);
setItems(prev => prev.filter(item => item !== itemToRemove));
I have working code that helps me create a to-do list. Everything works almost well. When I click on the "Enter" key, my page reloads. Similarly, after reloading the page, all created elements disappear. I have 2 questions: can you show how to save all created elements after reloading and how to avoid reloading by pressing on "Enter"? Thank you very much
import React, {useState} from "react";
export function Creating_List () {
let [allTasks, setAllTasks] = useState([]);
let [input, setInput] = useState('');
let addTask = (myInput) => {
if (myInput){
let newTask = {
id: Math.random().toString(36).substr(2,9),
task: myInput,
complete: false
}
setAllTasks([...allTasks, newTask])
}
}
let taskDone = (id) => {
setAllTasks([allTasks.filter((todo => todo.id !== id))])
}
let handleInput = (e) => {
setInput(e.currentTarget.value)
}
let submitTask = (e) => {
e.preventDefault();
addTask(input);
setInput('');
}
return (<div className='tasks'>
<h1>Список задач {allTasks.length}</h1>
<form>
<input
type="text"
value={input}
onChange={handleInput}
placeholder="Нове завдання"
/>
</form>
<button onClick={submitTask} type="submit">Створити</button>
<div>
{allTasks.map(el => <div key={el.id}>{el.task} <button onClick={taskDone}>Виконано</button> </div>)}
</div>
</div>)
}
Change your <form> to <form onSubmit = {submitTask}>, this will prevent the page from reloading on enter since you have e.preventDefault(). If you want data to persist after reloading, you can use localStorage or a database like Firebase or MongoDB, or you can create your own backend using Node.js.
I'm using a shorten URL API when the user passes a valid link, i fetch API and render the shortened URL with "map medthod" to make them into a list. It has a btn next to each mapped "shortened URL" where onClick i try to copyToClipboard and change state of btn from Copy to Copied. The problem is currently it only works fine if i have 1 item(on click btn works fine with copyToClipboard) but if i have 2 buttons and i click the very 1st btn to copyToClipboard it's focusing the last item in mapped list and copying the value of (last item) 2nd btn and also setting state for all btns to copied. I also don't understand why i can't see li tags with keys in console when i pass them the keys. can someone help me out. I just want to copyToClipboard that input value of the btn i have clicked. here's what it looks like - image of onCLick of 1st btn 2nd btn gets focus & image of no keys in console & apparently they aren't in a list?
Here is the code below
import { useForm } from "react-hook-form";
import axios from 'axios';
import Loading from '../../images/Ripple-1s-200px.svg';
const Shorten = () => {
// get built in props of react hook form i.e. register,handleSubmit & errors / watch is for devs
const { register, handleSubmit, formState: {errors} } = useForm();
//1. set user original values to pass as params to url
const [link, setLink] = useState('');
//2. set loader initial values to false
const [loading, setLoading] = useState(false);
//3. pass the fetched short link object into an array so we can map
const [displayLinks, setDisplayLinks] = useState([]);
//4. onSubmit form log data into link & showLoader for a breif moment
const onSubmit = (data, event) => {
event.preventDefault();
//puttin data in a variable to pass as url parameter if valid
setLink(data.userLink);
//add loading here after data is set to state
setLoading(!false);
}
//5. fetch the shortened url link using async method to show loading
useEffect(() => {
let unmounted = false;
async function makeGetRequest() {
try {
let res = await axios.get('https://api.shrtco.de/v2/shorten', { params: { url: link } });
//hid loader if u get response from api call
if (!unmounted && res.data.result.original_link) {
setLoading(false);
//add the data to displayLinks array to map
return setDisplayLinks(displayLinks => [...displayLinks, res.data.result]);
}
}
catch (error) {
console.log(error, "inital mount request with no data");
}
}
//invoke the makeGetRequest here
makeGetRequest();
return () => {
unmounted = true;
}
//passing dependency to re-render on change of state value
}, [link]);
//6. intial State of copied or not button
const [copySuccess, setCopySuccess] = useState('Copy');
const inputRef = useRef(null);
//7. onCick of button target it's short url right now it's selecting the last element
const copyToClipboard = (e) => {
e.preventDefault();
inputRef.current.select();
document.execCommand('copy');
// This is just personal preference.
setCopySuccess('Copied');
};
console.log(displayLinks);
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<label></label>
<input
{...register("userLink", {required: "Please add a link"})}
type="url"
id="userLink"
/>
{errors.userLink && <span>{errors.userLink.message}</span>}
<input type="submit" />
</form>
{
loading ?
<div className="loader" id="loader">
<img src={Loading} alt="Loading" />
</div>
: <ul>
{
displayLinks.map((el) => {
return (
<li key={el.code}>
<div>
<h5>{el.original_link}</h5>
</div>
{
/* Logical shortcut for only displaying the
button if the copy command exists */
document.queryCommandSupported('copy') &&
<form>
<input
ref={inputRef}
defaultValue={el.full_short_link}>
</input>
<button onClick={copyToClipboard}>{copySuccess}</button>
</form>
}
</li>
)
})
}
</ul>
}
</div>
)
}
export default Shorten;
Its because you are using a single ref for all the links
You are looping over all the links and giving their <input ref={inputRef} />.So the ref will always be attached to the last link input
Maybe don't use refs and use an alternative copyToClipboard function
like this one
const copyToClipboard = (url) => {
const textField = document.createElement('textarea')
textField.innerText = url
document.body.appendChild(textField)
if (window.navigator.platform === 'iPhone') {
textField.setSelectionRange(0, 99999)
} else {
textField.select()
}
document.execCommand('copy')
textField.remove()
setCopySuccess('Copied');
}
OR
Use a library like react-copy-to-clipboard
Also please go through this link
My problem is I print the list of movies with map and I want the list to automatically print after submit without refreshing the page:
const [movieName, setMovieName] = useState('');
const [movieReview, setMovieReview] = useState('');
const [movieReviewsList, setMovieReviewsList] = useState([]);
useEffect(() => {
Axios.get('http://localhost:3001/api/get')
.then((response) => {
setMovieReviewsList(response.data);
})
}, []);
const submitReview = () => {
Axios.post('http://localhost:3001/api/insert', {
movieName: movieName,
movieReview: movieReview,
});
setMovieReviewsList([...movieReviewsList, {
movieName: movieName,
movieReview: movieReview
}]);
}
After database submit, I add a newly added movie to the list and try to map it so that a new movie will appear without refreshing the page:
{movieReviewsList.map((movie) => {
return (
<div key={movie.idmovie_reviews}>
<h3>Movie name: {movie.movie_name} </h3>
<h3>Movie review: </h3>
<p>{movie.movie_review}</p>
</div>
);
})}
I don't add a key when inserting the value to the database, where in my SQL db it is auto-incremented. Is there any way to add it so that I will be able to map it without refreshing?
Your issue description is so short to solve.
I guess your form is submitted into server as sync.
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
...
}
<form onSubmit={onSubmit}>
// ...
<button type='submit'>submit</button>
</form>