So I am practicing React and wanted to display a "Arya's kill list" ], I wanted to make it possible to update it. So in my ToKill component when you double click on a character it shows inputs with values. But it is not possible to update them.
I wrote a function in my main App component it looks like this :
const toKillPpl = { ...this.state.toKill }
toKillPpl[index] = updatedToKill
this.setState({ toKillPpl })
}
next I pass it to ToKillList component with a state :
doubleClick = {this.doubleClickHandler}
deleteToKill = {this.deleteToKillHandler}
backBtn = {this.backBtnHandler}
state = {this.state}
toKillState = {this.state.toKill}
update = {this.toKillUpdate}
/>
in my ToKillList component I map over my state and I pass this function with a state of a person (toKillPerson) :
const ToKillList = (props) => props.state.toKill.map((toKill, index) => {
return <ToKill
double ={() => props.doubleClick(index)}
formDouble={toKill.formDouble}
click ={() => props.deleteToKill(index)}
backBtn ={() => props.backBtn(index)}
key={index + toKill.name}
index={index}
toKillPerson ={props.toKillState[index]}
update={props.update}
name={toKill.name}
cause={toKill.cause}
img={toKill.img}
/>
})
Finally in my ToKill component I write a function "handleChange" :
handleChange = (e) => {
const updatedToKill = {
...this.props.toKillPerson,
[e.currentTarget.name]: e.currentTarget.value
}
this.props.update(this.props.index, updatedToKill)
}
And here are inputs:
<input
type="text"
name="name"
className="hero-name"
onChange={this.handleChange}
value={this.props.name}
/>
<input
type="text"
name="img"
onChange={this.handleChange}
value={this.props.img}
/>
<input
type="text"
name="cause"
className="hero-cause"
onChange={this.handleChange}
value={this.props.cause}
/>
And it doesn't work. Is it a good approach, or I messed it up completely?
In case I wasn't clear here is a github repo: https://github.com/jakubmas/Aryas-Kill-List
Two correction in update method in your code.
1) You are not correctly copying over object,
const toKillPpl = { ...this.state.toKill }
This creates a shallow copy, you need deep cloning for this. You could either use JSON.strigify or lodash deepClone method.
2) You are not updating toKill state which is being passed to child components.
Here is the updated method:
toKillUpdate = (index, updatedToKill) => {
// const toKillPpl = JSON.parse(JSON.stringify(this.state.toKill)); // use this
const toKillPpl = _.cloneDeep(this.state.toKill); // or this
toKillPpl[index] = updatedToKill;
this.setState({ toKill: toKillPpl });
};
Here is the working codesandbox link
Hope that helps!!!
Another way to do this is that you could import immutability-helper (https://github.com/kolodny/immutability-helper), and use it to update state.toKill without mutating it:
import update from 'immutability-helper';
// Assuming 'updateToKill' input is an object...
handleUpdate = (index, updatedToKill) => {
this.setState(prevState => ({
toKill: update(prevState.toKill, {
[index]: {
$set: updatedToKill,
},
}),
}));
};
Related
I'm trying out React and trying to make a simple component. Input and "Add" button.
I want get a list of values after filling in the input and clicking on the button. I can see that the state is getting filled, but I don't understand why the list is not being rerender.
Here is my code https://jsfiddle.net/3hkm2qnL/14/
`
const InputWithAddBtn = props => {
const [ value, setValue ] = React.useState('');
return (
<div>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
<button onClick={() => props.add(value)}>+</button>
</div>
);
};
`
The problem is in the add() function, which by pushing onto the original array does not signal to the component to rerender.
const add = (value) => {
initValue.push(value)
console.log(initValue)
setValue(initValue)
}
One possible solution:
const add = (value) => {
const newValues = [...initValue, value]
console.log(newValues)
setValue(newValues)
}
That will trigger correctly the component to rerender.
For more info see https://stackoverflow.com/a/67354136/21048989
Cheers
You can check the full code here:
CODESANDBOX LINK
This is really weird.
I have a list of input fields which are rendered dynamically depending on the button click
const newFields = (index) =>
<div>
<input value={inputs[index].name} onChange={e => handleNameInput(e,index)} />
<input value={inputs[index].age} type="number" onChange={e => handleAgeInput(e,index)} />
<Button onClick={() => setInputArray(inputArray => [...inputArray, newFields(++index)])}>Add</Button>
</div>
I'm trying to store the values of the inputs and update them onChange. But as stated in the title, I can't type in the input field and once I click a character the input loses focus. I don't know why is that happening.
Here are the states and the handleInput function:
let [inputs, setInputs] = useState(Array.apply(null, Array(100)).map(function () { return {
name: '',
age: ''
} }));
const handleNameInput = (e, i) =>{
let tempArr = new Array();
tempArr = [...inputs];
tempArr[i].name = e.target.value;
setInputs([...tempArr]);
}
//handleAgeInput is exactly the same
I think your main issue is the duplicate usage of arrays. The facts is, you want only one array of datas. So, you need to build your component around it. If you make another array on state, you're adding complexity and it's resulting to you're current issue.
Understand why there is no updates
You're onChange function call update the inputs array state. BUT, you're rendering the inputArray where nothing change to him. No setInputArray call so, no re-render.
Proposal
There is proposal implementation of what I understand of you needs :
function App() {
let [inputArray, setInputArray] = useState([{
name: "",
age: ""
}]);
const handleNameInput = useCallback(
(e, i) => {
inputArray[i].name = e.target.value;
setInputArray([...inputArray]);
},
[inputArray]
);
const handleAgeInput = useCallback(
(e, i) => {
inputArray[i].age = e.target.value;
setInputArray([...inputArray]);
},
[inputArray]
);
const handleNewField = useCallback(() =>
setInputArray(inputArray => [...inputArray, {
name: "",
age: ""
}]),
[]
);
return (
<div className="App">
{inputArray.map((element, i) => (
<div>
<input
value={element.name}
onChange={e => handleNameInput(e, i)}
/>
<input
value={element.age}
type="number"
onChange={e => handleAgeInput(e, i)}
/>
<button
onClick={handleNewField}>
Add
</button>
</div>
))}
</div>
);
}
Depending of you needs maybe you should adapt it. There is some ways to avoid re-render the whole list. But this way you can edit all fields
One another proper way to do this is to to make each fields group as another Component.with independents states.
If you want the button juste once, move him from the loop.
If you want to display only the last fields set, take the inputArrays[inputArray.length] instead of loop on all.
Explanation :
You're rendering an array of components { inputArray.map((e, i) => { return <div key={i}> {e} </div>; })} where e should be a component.
In another hand, you're maintaining an array of datas that's "shadowing" the first array.
The main concept of your use case as far I can understand, should be to render your components directly from your datas – your array of datas.
This way you have only one source of truth.
I wish to generate inputs based on json, so first I set it to initial state, then in child componenet I want to modify it's field, thing is that component doesnt update... It renders once and have no idea how to make it be updated each time when input onChange change it's value. Any idea how to make value of input be updated each time when I type something?
PARENT
function App() {
const [inputValue, setInputValue] = useState(chunkingRow);
const handleChunkingChange = (e, index) => {
let inputContent = inputValue;
const reg = new RegExp(/^[0-9]+$/);
if (e.target.value.match(reg)) {
inputContent[index] = {
...inputContent[index],
content: e.target.value,
};
setInputValue(inputContent);
console.log(inputValue);
} else console.log('not a number')
};
return (
<div>
<Wrapper>
{Chunk(inputValue, handleChunkingChange)}
</Wrapper>
</div>
);
}
CHILD
const Chunk = (inputValue, handleChunkingChange) => {
return(
<div>
{inputValue.map((el, index) => (
<div key={index}>
<p>{el.title}</p>
{console.log(el.content)}
<input
type="text"
onChange={(e, i) => handleChunkingChange(e, index)}
value={el.content}
/>
</div>
))}
</div>
);
}
link to demo
https://codesandbox.io/s/stoic-mirzakhani-46exz?file=/src/App.js
Not completely sure why this happens, but probably because of the way you handle the input change. It seems to me that component doesn't recognize that array changed. How I managed to fix your code is replacing line 9 in App component with following code:
let inputContent = [...inputValue];
By doing that, array's reference is changed and components are updated.
Just update your code as follow:
let inputContent = [ ...inputValue ];
You are mutating the state object.
let inputContent = inputValue;
That's why the state is not re-rendered. Change it to
let inputContent = [...inputValue];
An example of mutating objects. React compares previous state and current state and renders only if they are different.
const source = { a: 1, b: 2 };
const target = source;
console.log(target);
console.log(target === source); = true
target.b = 99;
console.log({target});
console.log({source}); //source == target due to mutation
console.log(source === target); = true
Remember, never mutate.
I have seen other questions with the exact same title, but none solved my problem:
This is my code:
import React, { useState } from 'react';
import Validation from './Validation/Validation';
import Char from './Char/Char';
function App() {
const [textState, textSetState] = useState('');
const [charsState, charsSetState] = useState([]);
let inputChange = (event) => {
textSetState(event.target.value);
charsSetState(event.target.value.split('').map((char, index) => {
return <Char char={char} key={index} click={() => { deleteCharHandler(index) }} />
}));
}
const deleteCharHandler = (index) => {
alert(textState);
}
return (
<div className="App">
<input type="text" onChange={(event) => inputChange(event)} />
<Validation text={textState} />
{charsState}
</div>
);
}
export default App;
This is the result:
When I click a character, it displays the value from 1 step behind, like the example above.
You're putting an array of rendered Char components in your state, then rendering it. There are a number of issues with this approach.
The local copy of the state (charsState) will not be updated immediately; instead React will re-render the component with the new value for state.
Since you are defining the onClick callback within the function, deleteCharHandler will always be referencing an outdated copy of the state.
Multiple state hooks being updated in lockstep in this way will cause additional re-renders to happen.
Since the naming in your example is a bit confusing it's hard to tell what the desired behavior is to make good recommendations for how to resolve or refactor.
So there are a few things which may or may not be causing an issue so lets just clear a few things up:
You don't need to pass the anonymous function on the input:
<input type="text" onChange={inputChange} /> should suffice
As with OG state, it's never a good idea to call two setStates simultaneously, so lets combine the two:
const [state, setState] = useState({text: '', char: []});
Once you've updated everything you should be setting one state object onClick.
Your Char object is using click instead of onClick? unless you are using that as a callback method i'd switch to:
return <Char char={char} key={index} OnClick={() => deleteCharHandler(index)} />
If that doesn't fix your solution at the end, you can simply pass the deleteCharHandler the updated text value instead of re-grabbing the state value
I think you need useCallback or to pass textState in parameter. the deleteCharHandler method doesn't change in time with the textState value.
try :
return <Char char={char} key={index} click={() => { deleteCharHandler(index, textState) }} />
...
const deleteCharHandler = (index, textState) => {
alert(textState);
}
or :
import React, { useState, useCallback } from 'react';
...
const deleteCharHandler = useCallback(
(index) => {
alert(textState);
}, [textState]);
Try this
function App() {
const [textState, textSetState] = useState('');
const inputChange = useCallback((event) => {
textSetState(event.target.value);
},[])
function renderChars(){
return textState.split('').map((char, index) => {
return <Char char={char} key={index} click={() => { deleteCharHandler(index) }} />
}));
}
const deleteCharHandler = useCallback( (index) => {
alert(textState);
}, [textState])
return (
<div className="App">
<input type="text" onChange={inputChange} />
<Validation text={textState} />
{renderChars()}
</div>
);
}
I have some problems with testing events for nested components.
This is how my component tree look like:
- ModalComponent (Stateful with value for Input and update handler)
- - ModalType (stateless, passes value and update down to input)
- - - Input (stateless)
I have the value state and the handler for updating my value in my ModalComponent. This information just passed down through ModalType to my Input element via props.
I have tried to mount my ModalComponent with enzyme, find my input and simulate a change on the element. But this did not work.
What is the best strategy to test nested component when the handler and state are n parents components above?
EDIT
I have created a lean demo setup of my component in a separate blank react project
class App extends Component {
state = {
inputs: {
machineName: 'Empty'
}
}
onChangeHandler = (e) => {
let updatedState = null
console.log(e.target.value, e.target.id);
switch (e.target.id) {
case 'machineName':
updatedState = { ...this.state.inputs, machineName: e.target.value }
this.setState({inputs: updatedState})
break;
default:
break;
}
}
render() {
return (
<div className="App">
<ModalType onChange={this.onChangeHandler} value={this.state.inputs}></ModalType>
</div>
);
}
}
const ModalType = (props) => {
return <Input onChange={props.onChange} value={props.value.machineName}></Input>
}
const Input = (props) => (
<input id="machineName" onChange={props.onChange} value={props.value}></input>
)
My testing script
test('should handle change on input', () =>{
const wrapper = mount(<App/>)
wrapper.setState({inputs: { machineName: 'Empty' }})
wrapper.find('input').simulate('focus')
wrapper.find('input').simulate('change', {target: {value: '123'}})
wrapper.update()
// fails
expect(wrapper.state().inputs.machineName).toEqual('123')
// fails too
expect(wrapper.find('input').props().value).toEqual('123')
})
Thanks!
const wrapper = mount(<ModalComponent />);
const input = wrapper.find('input');
const event = {target: {value: 'testValue'}};
input.simulate('change', event);
The code above is a working example of how to simulate a change event on your input.
Edit
Your event is not correct. Since your handler is doing something only if the target id is machineName, you need to add that id to your mock event.
wrapper.find('input').simulate('change', {target: {value: '123', id: 'machineName'}})