make onHover affect only one element in reactjs - reactjs

im trying to show a button everytime a string is hovered, what im doing is working fine, but when i hover the string it will show every button in available string, i tried to pass the key but it still wont work, here is my code
const _showButton = () => {
setButton(true);
};
const _hideButton = () => {
setButton(false);
};
const _options = (uid) => {
return isButton ? <button key={uid}> ... </button> : null;
};
return(
{isProject.map((p) => {
return (
<div>
<Typography onMouseEnter={_showButton} onMouseLeave={_hideButton}>
{p.title} {_options(p.uid)}
</Typography>
</div>
);
})}
)
any help will be appreciated, thanks before, i know this question might be already asked before but i cant find the one that use a functional like me instead a class

it seems like button state is only one boolean, who controlled all mapped elements.
So you have two options,
First, change button state to an array
const [ button, setButton ] = useState(Array.from({ length: isProject.length }, _ => false))
and pass the index to functions and use specific slot to determine if a button should be visible
const _toggleButton = (i) => {
setButton(prev => prev.map((bool, idx) => i == idx ? !bool : bool);
};
const _options = (uid, i) => {
return button[i] ? <button key={uid}> ... </button> : null;
};
return(
{isProject.map((p, i) => {
return (
<div>
<Typography onMouseEnter={()=>_toggleButton(i)} onMouseLeave={()=>_toggleButton(i)}>
{p.title} {_options(p.uid, i)}
</Typography>
</div>
);
})}
)
Second is refactor mapped elemto it own component and declare state there, that way each elem will have it own state
{isProject.map((p) => <Component p={p} /> )}
function Component({p}) {
const [button, setButton] = useState(false)
const _showButton = () => {
setButton(true);
};
const _hideButton = () => {
setButton(false);
};
const _options = (uid) => {
return button ? <button key={uid}> ... </button> : null;
};
return (
<div>
<Typography onMouseEnter={_showButton} onMouseLeave={_hideButton}>
{p.title} {_options(p.uid)}
</Typography>
</div>
);
}

This is because _option runs in map and iterate over the whole list and the state is a single state which enables it for every item. You should consider setting "p's uid" in state variable (instead of true/false) and compare uid with the one is state in "_options" method.
const [selectedButtonUid, setSelectedButtonUid] = useState('');
const _showButton = (uid) => {
setSelectedButtonUid(uid);
};
const _hideButton = () => {
setSelectedButtonUid('');
};
const _options = (uid) => {
return selectedButtonUid === uid ? <button key={uid}> ... </button> : null;
};
return(
{isProject.map((p) => {
return (
<div>
<Typography onMouseEnter={()={_showButton(p.uid)}} onMouseLeave={_hideButton}>
{p.title} {_options(p.uid)}
</Typography>
</div>
);
})}
)

Related

React dynamic nested form input

Im a newbie in React and Im creating a simple form that sends data to DB. I made it work almost as I wanted, the only problem is that I dont know how to update the state which has an array inside.
The idea is to make a form so I can add recipes which include the whole recipe data that I map through to render each recipe. In the data object I need simple strings most of the time but then I need also three arrays or objects, I prefer the arrays in this case.
I found many solutions for class components but still I could figure out how to update the arrays. I even figured out how to update one array from a string input separated only with commas, then .split(', ') and .trim() and map() through but I could not setFormFields({}) at two places at the same time since the createRecipe() is async. The split just did not happen before the array was sent to the DB as a string. Thats why I dont put the whole code here.
I will simplify the code to make you see clear.
const defaultFormFields = {
title: '',
imageUrl: '',
leadText: '',
};
const NewRecipeForm = () => {
const [formFields, setFormFields] = useState(defaultFormFields);
const { title, imageUrl, leadText } = formFields;
const [ingredients, setIngredients] = useState([])
const handleFormFieldsChange = (event) => {
setFormFields({ ...formFields, [event.target.name]: event.target.value })
}
const handleIngredientsChange = ( event) => {
**// here I need help**
setIngredients()
}
const addIngredient = () => {
setIngredients([...ingredients, ''])
}
const removeIngredient = (index) => {
**// here I need help**
}
const createRecipe = async (event) => {
event.preventDefault()
// addRecipe sends the object to Firestore DB
addRecipe('recipes', url, formFields)
resetFormFields()
}
const resetFormFields = () => {
setFormFields(defaultFormFields);
};
return (
<NewRecipeFormContainer>
<h1>New recipe</h1>
<form onSubmit={createRecipe}>
<h1>Title</h1>
<input label='Title' placeholder='Recipe title' name='title' value={title} onChange={handleChange} />
<input label='imageUrl' placeholder='imageUrl' name='imageUrl' value={imageUrl} onChange={handleFormFieldsChange} />
<input label='leadText' placeholder='leadText' name='leadText' value={leadText} onChange={handleFormFieldsChange} />
<h1>Ingredients</h1>
**// here I need help ?**
{
ingredients.map((ingredient, index) => {
return (
<div key={index}>
<input label='Ingredience' placeholder='Ingredience' name='ingredient' value={ingredient.ingredient} onChange={handleChange} />
**// here I need help ?**
<button onClick={removeIngredient} >remove</button>
</div>
)
})
}
<button onClick={addIngredient} >add</button>
</form>
<Button onClick={createRecipe}>ODESLAT</Button>
</NewRecipeFormContainer>
)
}
I will appreciate any hint or help. Ive been totally stuck for two days. Thank you!
Here's an example of how to update a single element in a list.
const updateSingleItemInList = (index, update) => {
setList(list.map((l, i) => i === index ? update : l));
};
const add = (element) => setList([...list, element]);
Try simplifying your state first:
const [ingredients, setIngredients] = useState([]);
const [tips, setTips] = useState([]);
Then it becomes simple to write the handlers:
const updateIngredient = (index, text) => {
setIngredients(list.map((ing, i) => i === index ? text : ing));
};
const addIngredient = () => setIngredients([...ingredients, ""]);
Then you can create the form object when the user wants to submit:
addRecipe('recipes', url, {
ingredients: ingredients.map(i => ({ingredients: i})),
// etc.
});
Put it all together and here is the minimum viable example of a component that manages a dynamic number of form elements (tested, works):
export const TextBody = () => {
const [list, setList] = useState([{ name: "anything" }]);
const add = () => setList(l => [...l, { name: "" }]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text) => setList(l => l.map((ll, ii) => (ii === i ? { name: text } : ll)));
return (
<>
<TouchableOpacity onPress={add}>
<Text text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<>
<Text text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<Text text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text)} />
</>
))}
</>
);
};
You can return those CRUD functions and the state from a custom hook so you only have to write this once in a codebase.
Edit: Just for fun, here's the same component with a reusable hook:
const useListOfObjects = (emptyObject = {}, initialState = []) => {
const [list, setList] = useState(initialState);
const add = () => setList(l => [...l, emptyObject]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text, field) =>
setList(l => l.map((ll, ii) => (ii === i ? { ...ll, [field]: text } : ll)));
return {
list,
add,
remove,
update,
};
};
export const TextBody = () => {
const { list, add, remove, update } = useListOfObjects({ name: "", id: Math.random() });
return (
<>
<TouchableOpacity onPress={add}>
<TextBlockWithShowMore text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<React.Fragment key={`${l.id}`}>
<TextBlockWithShowMore text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<TextBlockWithShowMore text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text, "name")} />
</React.Fragment>
))}
</>
);
};

How to refresh only the updated item in a list, using useQuery() to get the list

I retrieve a list of jobs using useQuery(), each one have a Favourite icon (filled depending if it's favourited)
If I click that button, I managed to refresh the item Favourite icon, but it refreshes all the items.
Whats the correct way to avoid that? Because it appears the Loading wheel again, and I think it has to be a more elegant way.
const Openings = () => {
const onToggleFav = () => {
setFavCount(prev => prev + 1)
}
const [favCount, setFavCount] = useState(0);
const { isLoading, data } = useQuery(
['getRecruiterOpenings', favCount],
() => getRecruiterOpenings()
);
return (
<div>
{ isLoading ? <Loading /> : (
<>
{ data && data.openings && data.openings.map((opening) => (
<>
<Opening {...opening} onToggleFav={() => onToggleFav()} key={opening.id}/>
</>
))}
</>
)}
</div>
)
}
export default Openings;
Inside Opening component I have a method that dispatches when you click the fav icon:
const toggleFav = async (e) => {
e.preventDefault();
await toggleFavOpening(fav, id).then(() => {
if(onToggleFav) onToggleFav()
});
}

useState method not updating state with onClick?

I am creating a custom multiple choice question, but I am having difficulties updating my selection choice using useState.
const QuestionPage = ({ audioFiles }) => {
const [choice, setChoice] = useState(-1); // -1 is when none of the choices are selected
const updateChoice = val => {
setChoice(val);
}
return (
// each MultipleChoice contains an audio file and a radio button
<MultipleChoice audioFiles={audioFiles} choice={choice} updateChoice={updateChoice} />
)
};
const MultipleChoice = ({ audioFiles, choice, updateChoice }) => {
const answerOption = audioFiles.map((item, key) =>
<AudioButton file={file} index={key} choice={choice} updateChoice={updateChoice} />
);
return (
{answerOption}
);
}
const AudioButton = ({ file, index, choice, updateChoice }) => {
const handleClick = (val) => {
updateChoice(val);
};
const radioButton = (
<div className={`${index === choice ? "selected" : ""}`} onClick={() => handleClick(index)}>
</div>
);
return (
<>
{radioButton}
<Audio file={file} />
</>
);
}
In the first function, QuestionPage within updateChoice, when I use console.log(val), it updates according to the selections I make (i.e. 0 and 1). However, when I call console.log(choice), it keeps printing -1.
In addition, I keep getting an error message that says updateChoice is not a function.
Any advice? Thanks in advance!
Looks like you did not pass the value of audioFiles in MultipleChoice function

Cannot remove inputs array with filter

I am trying to remove an input field with filter function but it's not working.
In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can achieve removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders
So when I refresh the page and click to remove an input it will clear all other input data. How can I fix this problem ?
Update adding full component in question:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(
teamData.rules.map((el) => ({
...el,
guid: uuidV4(),
}))
);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const newList = inputs.filter((item, i) => index !== i); // <-- compare for matching index
setInputs(newList);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
// setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
// console.log("teamData.rules", teamData);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.guid}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
When i do console.log(inputs) this is the data that I got:
0: 0: "t" 1: "e" 2: "s" guid: "e18595a5-e30b-4b71-8fc2-0ad9c0e140b2"
proto: Object 1: 0: "d" 1: "a" 2: "s" 3: "d" 4: "a" 5: "s" guid: "537ca359-511b-4bc6-9583-553ea6ebf544" ...
Issue
The issue here is that you are using the array index as the React key. When you mutate the underlying data and reorder or add/remove elements in the middle of the array then the elements shift around but the React key previously used doesn't move with the elements.
When you remove an element then all posterior elements shift forward and the index, as key, remains the same so React bails on rerendering the elements. The array will be one element shorter in length and so you'll see the last item removed instead of the one you actually removed.
Solution
Use a React key that is intrinsic to the elements being mapped, unique properties like guids, ids, name, etc... any property of the element that guarantees sufficient uniqueness among the dataset (i.e. the siblings).
const [inputs, setInputs] = useState(teamData.rules);
const removeInputs = (index) => {
// compare for matching index
setInputs(inputs => inputs.filter((item, i) => index !== i));
};
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.id}> // <-- use a unique property
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
If your teamData.rules initial state value doesn't have any unique properties to use then you can map this to a new array and add a sufficient id property.
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: generateId()***,
})));
*** this is a function you need to define yourself, or import from a module like uuid***
import { v4 as uuidV4 } from 'uuid';
...
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: uuidV4(),
})));
// Add more input
const addInputs = () => {
setInputs(inputs => [
...inputs,
{
name: `rule_${inputs.length + 1}`,
guid: uuidV4();
},
]);
};
Then when mapping use the guid property.
<div className="agreement-form-grid" key={data.guid}>
The issue is because you are trying to compare index with array item in filter method. You should use the second argument in filter which denotes the array index of the current iterating item
const removeInputs = (index) => {
const newList = inputs.filter((item,i) => index !== i);
setInputs(newList);
};
That's your solution, you are trying with item but you are comparing it with index that's wrong. You should do it like this,
const newList = inputs.filter((item, key) => index !== key);

How can you make a createRef/useRef not target the last value. But rather go to where its suppose to

Can't manage to make useRef/createRef to get any other div's other then what was added last. How can i make it so when the button is clicked the ref to the div changes.
I've tried with both useRef and createRef. Since I want to make a new instance of ref, i've looked more into createRef rather then useRef.
I've also played around useEffect. But my solution didn't help me with my biggest problem
I have made a small project containing 3 components to help you understand what I'm trying to explain.
I also have a database containing mock data -> in my real project this isn't the problem. It's an array containing objects.
[{'id':'1', 'name':'first'},...]
Main:
const MainComponent = () => {
const dataRef = React.createRef(null)
React.useEffect (() => {
if(dataRef && dataRef.current){
dataRef.current.scrollIntoView({ behavior:'smooth', block:'start' })
}
},[dataRef])
const _onClick = (e) => {
dataRef.current.focus();
}
return(
<>
{data && data.map((entry, index) =>{
return <ButtonList
key={index}
entry={entry}
onClick={_onClick}
/>
})}
{data && data.map((entry, index) =>{
return <ListingAllData
key={index}
dataRef={dataRef}
entry={entry}
index={index}/>
})}
</>
)
}
Button Component
const ButtonList = ({ entry, onClick }) => {
return <button onClick={onClick}>{entry.name}</button>
}
Listing data component
const ListingAllData = (props) => {
const {entry, dataRef } = props;
return (
<div ref={dataRef}>
<p>{entry.id}</p>
<p>{entry.name}</p>
</div>
);
}
I've console logged the data.current, it only fetches the last element. I hoped it would fetch the one for the button I clicked on.
I think the main idea here is to create dynamic refs for each element (array of refs), that's why only the last one is selected when app renders out.
const MainComponent = () => {
const dataRefs = [];
data.forEach(_ => {
dataRefs.push(React.createRef(null));
});
const _onClick = (e, index) => {
dataRefs[index].current.focus();
dataRefs[index].current.scrollIntoView({
behavior: "smooth",
block: "start"
});
};
return (
<>
{data &&
data.map((entry, index) => {
return (
<ButtonList
key={index}
entry={entry}
onClick={e => _onClick(e, index)}
/>
);
})}
{data &&
data.map((entry, index) => {
return (
<>
<ListingAllData
key={index}
dataRef={dataRefs[index]}
entry={entry}
index={index}
/>
</>
);
})}
</>
);
};
Created working example in code sandbox.
https://codesandbox.io/s/dynamic-refs-so25v
Thanks to Janiis for the answer, my solution was:
in MainComponent
...
const refs = data.reduce((acc, value) => {
acc[value.id] = React.createRef();
return entry;
}, {});
const _onClick = id => {
refs[id].current.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
....
then i passed it through to the child and referred like
<div ref={refs[entry.id]}>

Resources