A very simple list with inputs that are supposed to be addable and updatable.
The problem occurs after I add one or more inputs and then try to type inside of one of the inputs - all inputs after the one being typed in disappear.
It has something to do with memo-ing the Item component and I'm looking to understand what exactly is happening there (valueChanged getting cached?). I can't wrap my head around.
Without the memo function the code works as expected but of course, all inputs get updated on every change.
Here's a gif of what's happening: https://streamable.com/gsgvi
To replicate paste the code below into an HTML file or take a look here: https://codesandbox.io/s/81y3wnl142
<style>
ul {
list-style-type:none;
padding:0;
}
input[type=text] {
margin:0 10px;
}
</style>
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone#6.15.0/babel.min.js"></script>
<script type="text/babel">
const randomStr = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
const { useState, memo, Fragment } = React
const Item = memo(({ value, i, valueChanged }) => {
console.log('item rendering...');
return <li>
<input type='text' value={value} onChange={e => valueChanged(i, e.target.value)}/>
</li>
}, (o, n) => o.value === n.value)
const ItemList = ({ items, valueChanged }) => {
return <ul>{
items.map(({ key, title }, i) => (
<Item
key={key}
i={i}
value={title}
valueChanged={valueChanged}
/>
))
}</ul>
}
const App = () => {
const [items, setItems] = useState([
{ key: randomStr(), title: 'abc' },
{ key: randomStr(), title: 'def' },
])
const valueChanged = (i, newVal) => {
let updatedItems = [...items]
updatedItems[i].title = newVal
setItems(updatedItems)
}
const addNew = () => {
let newItems = [...items]
newItems.push({ key: randomStr(), title:'' })
setItems(newItems)
}
return <Fragment>
<ItemList items={items} valueChanged={valueChanged}/>
<button onClick={addNew}>Add new</button>
</Fragment>
}
ReactDOM.render(<App/>, document.querySelector('#app'))
</script>
valueChanged is a closure which gets a fresh items every render. But your memo doesn't trigger an update because it only checks (o, n) => o.value === n.value. The event handler inside Item uses the old items value.
It can be fixed with functional updates:
const valueChanged = (i, newVal) => {
setItems(oldItems => {
let updatedItems = [...oldItems];
updatedItems[i].title = newVal;
return updatedItems;
});
}
So valueChanged doesn't depend on items and doesn't need to be checked by memo.
Your code might have similar problems with other handlers. It is better to use functional updates whenener new value is based on the old one.
You can try initializing the state this way, I tried it on codesandbox and it may be the behavior you are looking for.
https://codesandbox.io/s/q8l6m2lj64
const [items, setItems] = useState(() => [
{ key: randomStr(), title: 'abc' },
{ key: randomStr(), title: 'def' },
]);
Related
I have several HTML img element I want to manipulate on the following events:
onMouseEnter
onMouseLeave
onMouseDownCapture
onMouseUp
A naive solution (that works) is to implement these listeners for each event manually as such:
<img
src={taskbarImage}
onMouseEnter={(e) =>setTaskbarImage(taskbarAppImageHover)}
onMouseLeave={(e) => setTaskbarImage(taskbarAppImage)}
onMouseUp={(e) => setTaskbarImage(taskbarAppImageHover)}
onMouseDownCapture={(e) => setTaskbarImage(taskbarAppImageFocus)}
className="taskbar-application-img">
</img>
This code is kind of messy and I would much rather simply attach one function that triggers any time any event happens on the tag. After this, the function would then analyze for what event it is and act appropriately. Something like this:
const taskBarManipulation = (e) => {
switch (e.type) {
case "mouseenter":
setTaskbarImage(taskbarAppImageHover);
case "mouseleave":
setTaskbarImage(taskbarAppImageHover);
case "mouseup":
setTaskbarImage(taskbarAppImage);
case "mousedowncapture":
setTaskbarImage(taskbarAppImageFocus);
}
};
The snippet above works for detecting the type of event and changing the variable. However, I don't know how to make the function trigger on any event happening in the tag. Any suggestions?
There are many events, listening tho all of those will slow down your component, and is not recommended.
I'd use a function that returns the eventListeners you wish to add, and then apply that to the component using spreading:
const { useState } = React;
const getEvents = () => {
return {
onClick: () => console.log('onClick'),
onMouseEnter: () => console.log('onMouseEnter'),
onMouseLeave: () => console.log('onMouseLeave'),
// ...etc
};
}
const Example = () => {
return (
<div>
<h1 {...getEvents()}>{'Test me!'}</h1>
</div>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
If all those event have the same handler, we can create a more fancy getEvents function like so:
const eventHandler = (e) => console.log(e.type);
const getEvents = (events = [ 'onClick', 'onMouseEnter', 'onMouseLeave' ]) => {
return events.reduce((c, p) => ({ ...c, [p]: eventHandler }), {});
}
const { useState } = React;
const eventHandler = (e) => console.log(e.type);
const getEvents = (events = [ 'onClick', 'onMouseEnter', 'onMouseLeave' ]) => {
return events.reduce((c, p) => ({ ...c, [p]: eventHandler }), {});
}
const Example = () => {
return (
<div>
<h1 {...getEvents()}>{'Test me!'}</h1>
</div>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I'm trying to filter some data by their category id. Right now I can filter by category with value: 1. What I am trying to achieve is when the button is clicked again, it should clear the filter. Here is the code below:
const onPress = () => {
const filteredResults = results.filter((result) =>{
return result.category === 1;
});
setResults(filteredResults);
};
The button:
<Button onPress={() =>{onPress()}}>Lajme</Button>
How can I implement this function in the same button:
const clearFilter = () => {
const filteredResults = results
setResults(filteredResults);
};
Store the current filter (category) with useState(), and whenever you update it (see toggleFilter) check if the current filter is already set. If it is, reset (change to null for example).
const { useState, useMemo } = React;
const Demo = ({ results }) => {
const [category, setCategory] = useState(null);
const filteredResults = useMemo(() => results.filter(result => category === null || result.category === category), [results, category]);
const toggleFilter = cat => setCategory(c => cat === c ? null : cat);
return (
<div>
<button onClick={() => toggleFilter(1)}>Cat 1</button>
<button onClick={() => toggleFilter(2)}>Cat 2</button>
<div>
{JSON.stringify(filteredResults)}
</div>
</div>
);
};
const results = [{ category: 1 }, { category: 2 }, { category: 2 }, { category: 1 }]
ReactDOM.render(
<Demo results={results} />,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Basically, you can use a state variable to know if the filter is currently applied or not.
const [isFiltered, setFiltered] = useState(false);
const onPress = () => {
if (isFiltered) {
setResults(results); // Update results without filter
setFiltered(false); // Update the state
} else {
const filteredResults = results.filter(({ category }) => category === 1); // Filter the results
setResults(filteredResults); // Update results
setFiltered(true); // Update the state
}
};
// ...
<Button onPress={onPress}>Lajme</Button>
const allTodos = [{id: 1, name: 'whatever user type'}, { }, { }, .... { }] // Defined as an array using setState in other file. Imported in this file as allTodos using props.
export const Todos = (props) => {
const [index, setIndex] = useState(0)
props.allTodos.map((prev) => {
return (
<div id="item_container">
<button type='button' className = `check_button ${prev.id === index ? 'checked' : 'not_checked'}`
onClick = {() => setIndex(prev.id)}>
<img src = {check} alt="" id = "check_img"></img>
</button>
<li id="li_item">{prev.name}</li>
</div>
)}
}
Explanation : Here, I set up a const index using useState that wiil change its value to the id of the element clicked upon in order to change the className of that element.
Question : Now, I succeeded in doing that but everytime I click on other element, the className gets added to that element but gets removed from the previous element it was added upon. Now I want to somehow preserve the className to those every elements I click on until I click on them again to change their className. By the way the styling that I desire to change is the background of that button/element.
You need to be able to keep track of every index that's checked - for this you'll need an array (or a number you do bit calculations with). A stateful index number doesn't contain enough information.
const allTodos = [{ id: 1, name: 'whatever user type' }, { id: 2, name: 'foo' }];
const Todos = ({ allTodos }) => {
const [todos, setTodos] = React.useState(() => allTodos.map(todo => ({ ...todo, checked: false })));
const handleClick = (i) => () => {
setTodos(todos.map((todo, j) => j !== i ? todo : ({ ...todo, checked: !todo.checked })))
};
return todos.map(({ id, name, checked }, i) => (
<div id="item_container">
<button
type='button'
className={`check_button ${checked ? 'checked' : 'not_checked'}`}
onClick={handleClick(i)}
>
<img alt="" id="check_img"></img>
</button>
<div>{name}</div>
</div >
))
}
ReactDOM.render(<Todos allTodos={allTodos} />, document.querySelector('.react'));
.checked {
background-color: green;
}
.not_checked {
background-color: yellow;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
You also need:
When not using plain ' " quotes, use {} delimiters around the prop
<li>s should only be children of <ul>s
Duplicate IDs are invalid HTML
You need to return the mapped JSX from the Todos component
You are missing a ) at the end of allTodos.map
You should use array to save ids clicked and removed id from array when clicked again.
const [idList, setIdList] = useState([])
const handleClick=(id, checked) => {
if(checked){
setIdList.filter(item => item !== id)
} else {
setIdList([...idList, id])
}
}
props.allTodos.map((prev) => {
const checked = idList.includes(prev.id)
...
className ={`check_button ${checked ? 'checked' : 'not_checked'}`}
onClick = {() => handleClick(prev.id, checked)
...
I want to create a wizard that changes content when the user clicks a "next" button.
I'm currently trying to use the .map function, it works but how can I adjust my code to loop over each step in my array onClick?
Currently, my code just displays 3 separate inputs with all of the steps in the array, what I want to do is iterate over each step onClick.
Here is an example of my code:
Array:
const wizardControls = {
steps: [
{
step: 1,
name: 'name1',
type: 'text',
placeholder: 'name1',
},
{
step: 2,
name: 'name2',
type: 'text',
placeholder: 'name2',
},
{
step: 3,
name: 'name3',
type: 'text',
placeholder: 'name3',
},
],
};
JSX using map() function:
{steps.map((step, index) => (
<div key={index}>
<input
value={value}
name={step.name}
type={step.type}
placeholder={step.placeholder}
onChange={onChange}
/>
</div>
))}
I'm thinking the button will need a handler function to loop over the index, however, I'm unsure how to do this with the map() function.
I'm open to a better approach if the map() function isn't the best route.
One way you could do this is by slicing by which step you're on (based on index).
Here's an example of what that might look like with your code.
const [step, setStep] = useState(1)
...
steps.slice(step - 1, step).map((step, index) => (
...
))
See a working example here: https://codesandbox.io/s/pensive-northcutt-el9w6
If you want to show a step at a time, don't use Array.map() to render all of them. Use useState to hold the current index (step), and take the current item from the steps array by the index. To jump to the next step, increment the index by 1.
const { useState } = React;
const Demo = ({ steps }) => {
const [index, setIndex] = useState(0);
const [values, setValue] = useState([]);
const next = () =>
setIndex(step => step < steps.length -1 ? step + 1 : step);
const onChange = e => {
const val = e.target.value;
setValue(v => {
const state = [...v];
state[index] = val;
return state;
})
};
const step = steps[index];
return (
<div>
<input
value={values[index] || ''}
name={step.name}
type={step.type}
placeholder={step.placeholder}
onChange={onChange}
/>
<button onClick={next}>Next</button>
</div>
);
};
const wizardControls = {"steps":[{"step":1,"name":"name1","type":"text","placeholder":"name1"},{"step":2,"name":"name2","type":"text","placeholder":"name2"},{"step":3,"name":"name3","type":"text","placeholder":"name3"}]};
ReactDOM.render(
<Demo steps={wizardControls.steps} />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
I have the following code which add checkboxes in my page. The question is how can I get their values? What I am currently doing is using onChange to get the values and add it to my state but not sure if that's the best way? Whats the best practice in collecting data in input, should I use onChange or ref, I am a little bit confused with ref(I am new to ReactJS)
{Object.keys(this.state.IndustryChoice).map(key => (
<label className="custom-control fill-checkbox">
<input
name="Industry"
type="checkbox"
value={this.state.IndustryChoice[key]}
className="fill-control-input"
onChange={this.props.onMainToggle}
/>
<span className="fill-control-indicator" />
<span className="fill-control-description">
{this.state.IndustryChoice[key]}
</span>
</label>
))}
Here are other parts of my code
handleMainObjectCheckBoxToggle = event => {
let field = event.target.name;
let value = event.target.value;
let MainObject = this.state.MainObject;
// console.log("Winning Ways", MainObject[field]);
if (customIncludes(MainObject[field].results, value)) {
MainObject[field].results = customRemoveStringArray(
MainObject[field].results,
value
);
} else {
MainObject[field].results = appendObjTo(
MainObject[field].results,
value
);
// console.log(MainObject[field].results);
}
return this.setState({ MainObject });
};
<FormTactics
onMainToggle={this.handleMainObjectCheckBoxToggle}
/>
You must put checkbox values in your state as it gives your component more power to control the checkbox updates. Just use setState on onChange method and you'll see updated checkboxes
Let me know if you need more help
I can't quite understand your code but it seems you are trying to push or remove items somewhere in the state. I don't know if it suits you but I am providing a simple solution. Main differences with your code:
Hardcoded Industry key.
I'm not mutating the state directly which is a good behavior.
I am using the checked value for the elements.
I'm not quite sure how you implement this code in your app and if you need the checked values elsewhere. Here, we are keeping the checked values per value in the state in case you use them in the future.
class App extends React.Component {
state = {
IndustryChoice: {
foo: "foo",
bar: "bar",
baz: "baz"
},
MainObject: {
Industry: {
results: []
}
},
checkedValues: {}
};
handleMainObjectCheckBoxToggle = e => {
const { value, checked } = e.target;
const { results } = this.state.MainObject.Industry;
if (checked) {
const newResults = [...results, value];
this.setState(prevState => ({
MainObject: {
...prevState.MainObject,
Industry: { ...prevState.MainObject.Industry, results: newResults }
},
checkedValues: { ...prevState.checkedValues, [value]: checked }
}));
} else {
const newResults = results.filter(el => el !== value);
this.setState(prevState => ({
MainObject: {
...prevState.MainObject,
Industry: { ...prevState.MainObject.Industry, results: newResults }
},
checkedValues: { ...prevState.checkedValues, [value]: checked }
}));
}
};
render() {
return (
<div>
{Object.keys(this.state.IndustryChoice).map(key => (
<label>
<input
name={key}
type="checkbox"
value={this.state.IndustryChoice[key]}
className="fill-control-input"
onChange={this.handleMainObjectCheckBoxToggle}
/>
<span>{this.state.IndustryChoice[key]}</span>
</label>
))}
<p>results: {JSON.stringify(this.state.MainObject.Industry.results)}</p>
<p>checked: {JSON.stringify(this.state.checkedValues)}</p>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>