React Form not updating state onChange - javascript

I'm trying to practice React forms, and I can't seem to solve this error. I've developed a multi-input form and I need the value to update the parent's state. When I start typing in the input field it triggers the Switch case default. I'm not sure if maybe I'm typing the 'handleChange' function wrong or if I should have a separate state for the state.data. Any advice would be much appreciated
import React, { useState } from 'react';
import Form from './Form';
import Results from './Results';
function App() {
const [state, setState] = useState({
data: 1,
Name: '',
Email: '',
City: ''
});
const nextStep = () => {
setState({
data: state.data + 1
});
};
const handleChange = e => {
let field = e.target.name;
let val = e.target.value;
setState({ [field]: val });
};
switch (state.data) {
case 1:
return (
<div className='App-container'>
<Form
button='Next'
nextStep={nextStep}
name='Name'
state={state.name}
handleChange={handleChange}
/>
</div>
);
case 2:
return (
<div className='App-container'>
<Form
button='Next'
nextStep={nextStep}
name='Email'
state={state.email}
handleChange={handleChange}
/>
</div>
);
case 3:
return (
<div className='App-container'>
<Form
button='Submit'
nextStep={nextStep}
name='City'
state={state.city}
handleChange={handleChange}
/>
</div>
);
case 4:
return (
<div className='App-container'>
<Results data={state} />
</div>
);
default:
return 'Error';
}
}
export default App;
import React from 'react';
const Form = ({ button, nextStep, name, state, handleChange }) => {
const handleSubmit = e => {
e.preventDefault();
nextStep();
};
return (
<div className='Form-container'>
<form onSubmit={handleSubmit}>
<input
type='text'
placeholder={name}
name={name}
value={state}
onChange={handleChange}
/>
<input type='submit' value={button} />
</form>
</div>
);
};
export default Form;
import React from 'react';
const Results = ({ data }) => {
return (
<div>
<h1>Info</h1>
<p>{data.name}</p>
<p>{data.email}</p>
<p>{data.city}</p>
</div>
);
};
export default Results;

You losing state on handleChange while expecting it to merge with the current one, but it is not how useState works in contradiction to this.setState in class components:
// Results new state: { data };
setState({ data: state.data + 1 });
// Instead update the copy of the state.
// Results new state: { data, Name, Email, City }
setState({ ...state, data: state.data + 1 });
Check useState note in docs:
Note
Unlike the setState method found in class components, useState does
not automatically merge update objects. You can replicate this
behavior by combining the function updater form with object spread
syntax.
Another option is useReducer,
which is more suited for managing state objects that contain multiple
sub-values.

I recommend using a custom hook for form inputs.like follows
//useForm.js
const useForm = defaultValues => {
const [values, setValues] = useState(defaultValues);
const handleChange = ({ name, value }) => {
setValues(prevState => ({ ...values, [name]: value }));
};
const reset = () => {
setValues(null);
};
return [values, handleChange, reset];
};
inside component
const [formValues, handleChange, resetForm] = useForm();
return (
<>
<Input
value={formValues.name}
onChange: str => handleChange({ name: "name", value: str })
/>
<Input
value={formValues.email}
onChange: str => handleChange({ name: "email", value: str })
/>
</>
)

You need to spared old state
const handleChange = e => {
let field = e.target.name;
let val = e.target.value;
setState({ ...state, [field]: val });
};
this is setState({ ...state, [field]: val });

Related

Why is my empty object in useState hook rendering?

I'm just refreshing myself on functional components and react state hooks, building a simple react todo list app- all the simple functionalities are built out but I have this one bug during initial state where there is an empty task rendering in the list. What am I missing? Any help would be greatly appreciated. :)
App.js:
import TodoList from './TodoList'
function App() {
return (
<div>
<TodoList />
</div>
);
}
export default App;
Todolist.js:
import React, {useState} from 'react'
import NewTodoForm from './NewTodoForm'
import Todo from './Todo'
const TodoList = () => {
const [state, setState] = useState({
list: [{title: "", id: ""}]
})
const addTodo = (newTodo) => {
setState({
list: [...state.list, newTodo]
})
console.log('after state change in addtodo', state.list.title)
}
const remove = (toDoId) => {
console.log('logging remove')
setState({
list: state.list.filter(todo => todo.id !== toDoId)
})
}
const strike = e => {
const element = e.target;
element.classList.toggle("strike");
}
const update = (id, updatedTask) => {
//i cant mutate state directly so i need to make a new array and set that new array in the state
const updatedTodos = state.list.map(todo => {
if (todo.id === id) { // find the relevant task first by mapping through existing in state and add updated info before storing it in updatedtodos
return { ...todo, title: updatedTask}
}
return todo
})
console.log('updated todos', updatedTodos)
setState({
list: updatedTodos
})
console.log('list after updating state')
}
return (
<div className="TodoList">
<h1>Todo List<span>A simple react app</span></h1>
<NewTodoForm addingTodo={addTodo}/>
{ state.list.map(todo => <Todo id={todo.id} key={todo.id} title={todo.title} updateTodo={update} strikeThrough={strike} removeTodo={() => remove(todo.id)} />) }
</div>
)
}
export default TodoList
Todo.js:
import React, {useState} from 'react'
const Todo = ({id, title, removeTodo, strikeThrough, updateTodo}) => {
const [state, setState] = useState({
isEditing: false,
})
const [task, setTask] = useState(title);
const handleUpdate = (e) => {
e.preventDefault()
updateTodo(id, task)
setState({ isEditing: false})
}
const updateChange = (e) => {
// setState({...state, [e.target.name]: e.target.value})
setTask(e.target.value)
console.log(task)
}
return (
<div>
{state.isEditing ?
<div className="Todo">
<form className="Todo-edit-form" onSubmit={handleUpdate}>
<input
type="text"
value={task}
name="task"
onChange={updateChange}
>
</input>
<button>Submit edit</button>
</form>
</div> :
<div className="Todo">
<ul>
<li className="Todo-task" onClick={strikeThrough}>{title}</li>
</ul>
<div className="Todo-buttons">
<button onClick={() => setState({isEditing: !state.isEditing})}><i class='fas fa-pen' /></button>
<button onClick={removeTodo}><i class='fas fa-trash' /></button>
</div>
</div>
}
</div>
)
}
export default Todo
You're rendering your to-do's with:
{ state.list.map(todo => <Todo id={todo.id} key={todo.id} title={todo.title} updateTodo={update} strikeThrough={strike} removeTodo={() => remove(todo.id)} />) }
Your initial state is:
{ list: [{title: "", id: ""}] }
The above state will cause React to render an empty to-do item for you. Once you clear the array, you should not see anything. Another option is to change your rendering and add a conditional that checks if to-do item values are empty, to not render them.
The initial state in TodoList seems to be having list:[{title: "", id: ""}], which contains empty title and id. Since it's mapped to create Todo, I think it starts with an empty Todo.

Custom Output in react-select

I use react-select and I'm new.I have a component called Example
import React from "react";
import Select from "react-select";
class Example extends React.Component {
state = {
selectedOption: null
};
render() {
const { onHandleChange, options } = this.props;
return <Select onChange={onHandleChange} options={options} isMulti />;
}
}
export default Example;
In another file we have a functional Component
import React, { useState } from "react";
import Example from "./Example";
import { regionOptions, ageOptions, bordOptions } from "./Options";
export default function UserProfile() {
const [selectedOption, setSelectedOption] = useState({
region: "",
age: "",
bord: ""
});
const handleChange = (key, selectedOption) => {
setSelectedOption(prev => ({ ...prev, [key]: selectedOption }));
};
console.log(Object.values(selectedOption));
return (
<div>
<Example
id="region"
onHandleChange={value => handleChange("region", value)}
selectedOption={selectedOption.region}
options={regionOptions}
/>
<Example
id="age"
onHandleChange={value => handleChange("age", value)}
selectedOption={selectedOption.age}
options={ageOptions}
/>
<Example
id="bord"
onHandleChange={value => handleChange("bord", value)}
selectedOption={selectedOption.bord}
options={bordOptions}
/>
</div>
);
}
I display the values in the console by the handChange event.
But when the options increase, I can't say which one belongs to which .
I want the console.log instead of the
[Array[n], Array[n], Array[n]]
Something like this will be displayed
[Region[n], age[n], bord[n]]
You can see my code here
https://codesandbox.io/s/upbeat-night-tqsk7?file=/src/UserProfile.js:0-1040
just use
console.log(selectedOption);
instead of
console.log(Object.values(selectedOption));
What you can do is a create a custom hook and make the following changes.
// custom hook
function useFormInput(initial) {
const [value, setValue] = useState(initial);
const handleOnChange = e => {
setValue(e);
};
return {
selectedOptions: value,
onChange: handleOnChange
};
}
then in the code
export default function UserProfile() {
const region = useFormInput(""); // return { selectedOption, onChange }
const age = useFormInput("");
const bord = useFormInput("");
// NB {...region} pass deconstructed return values from custom hook to the component
return (
<div>
<Example id="region" {...region} options={regionOptions} />
<Example id="age" {...age} options={ageOptions} />
<Example id="bord" {...bord} options={bordOptions} />
{JSON.stringify(region.selectedOptions)}
{JSON.stringify(age.selectedOptions)}
{JSON.stringify(bord.selectedOptions)}
</div>
);
}
// your UI component
render() {
const { onChange, options } = this.props;
return <Select onChange={onChange} options={options} isMulti />;
}
working example
https://codesandbox.io/s/optimistic-napier-fx30b?

How to rewrite a React class that extends another class as a functional component?

I have the following simple code and I am trying to rewrite it as a function avoiding classes and using hooks for learning purposes.
As you can see below, 'App' is extending 'Form'. The complete code includes other functions in 'Form', for example, a validation function which is called by 'handleChange' and modifies the 'errors' item in the state.
Note that 'Form' is not part of 'App' because Form will be reused by other components (such as a login component).
My main questions:
1- As per the documentation, they seem to discourage the use of Inheritance, how can I rewrite this without using "extends" (and keeping the classes)?
2- How can I rewrite this without classes?
So far, the only idea that came to my mind is to rewrite all the functions in form.jsx as independent functions and call them from App (see below). But this implies to write a lot of props and parameters (specially when validation is added as 'errors', 'setErrors', 'schema', etc. would be sent from 'App' to 'renderInput', from here to 'handleChange', etc.
It works but the code is less clean than before...
app.js
class App extends Form {
state = {
data: { username: "", password: "" },
};
render() {
return (
<form action="">
{this.renderInput("username", "Username")}
{this.renderInput("password", "Password", "password")}
</form>
);
}
}
form.jsx
class Form extends Component {
state = {
data: {},
};
handleChange = ({ currentTarget }) => {
const data = { ...this.state.data };
data[currentTarget.name] = currentTarget.value;
this.setState({ data });
};
renderInput(name, label, type = "text") {
const { data, errors } = this.state;
return (
<Input
name={name}
type={type}
value={data[name]}
label={label}
onChange={this.handleChange}
/>
);
}
render() {
return null;
}
}
export default Form;
input.jsx
const Input = ({ name, label, ...rest }) => {
return (
<div className="form-group">
<label htmlFor={name}>{label}</label>
<input {...rest} name={name} id={name} className="form-control" />
</div>
);
};
Attempt to change it into functions:
App.jsx
const App = () => {
const [user, setUser] = useState({ username: "", password: "" });
return (
<form action="">
{renderInput("username", "Username", user, setUser)}
{renderInput("password", "Password", user, setUser, "password")}
</form>
);
};
form.jsx
export function handleChange({ currentTarget }, data, setData) {
setData({ ...data, [currentTarget.name]: currentTarget.value });
}
export function renderInput(name, label, data, setData, type = "text") {
return (
<Input
name={name}
type={type}
value={data[name]}
label={label}
onChange={e => handleChange(e, data, setData)}
/>
);
}
Thanks in advance and let me know if you need a better explanation or the full code.
Move the form to the Form component and pass an array of inputs' properties to generate inputs:
App.jsx
const App = () => {
const [user, setUser] = useState({ username: "", password: "" });
const inputList = [
{name: "username", label: "Username", value: user.username},
{name: "password", label: "Password", value: user.password, type: "password"}
]
return (
<Form inputList={inputList} setData={setUser} />
);
};
Form.jsx
const Form = ({ inputList, setData }) => {
const handleChange = ({ currentTarget }) => {
const { name, value } = currentTarget;
setData(prevData => ({ ...prevData, [name]: value }));
};
return (
<form action="">
{
inputList.map(({ name, label, value, type = "text" }) =>
<Input
key={name}
name={name}
type={type}
value={value}
label={label}
onChange={handleChange}
/>
)
}
</form>
);
}

input target value from child component

I've managed to get my input component to render onto the dom, however I'm having a bit of trouble accessing the props.
Functional input component
const InputField = props => {
const { inputValue, updateInputValue } = props
return (
<Input>
<input
type="text"
placeholder="Please specify"
value={inputValue}
onChange={updateInputValue}
/>
<hr />
<label>Other</label>
</Input>
)
}
The component is only rendered to the dom based on an object property inside of an array
const MultiChoiceQuestions = props => {
const { multiChoiceArray, handleClick } = props
return (
<ButtonContainer>
{multiChoiceArray.map(questionChoice => {
if (questionChoice.type === 'input') {
return <InputField />
}
return (
<Button type="button" key={questionChoice.id} onClick={() => handleClick(questionChoice)}>
{questionChoice.text}
</Button>
)
})}
</ButtonContainer>
)
}
The multiChoice component is imported once again to create a top-level component that the app consumes
const Question = props => {
let responses
switch (props.data.type) {
case 'multiChoice':
responses = (
<MultiChoiceQuestions
multiChoiceArray={props.data.choices}
handleClick={props.handleClick}
inputValue={props.inputValue}
updateInputValue={props.updateInputValue}
/>
)
break
default:
responses = <div>Error: no question type: `{props.data.type}`</div>
}
const { data } = props
return (
<AnimatedDiv key={data.id}>
<QuestionText>{data.text}</QuestionText>
{responses}
</AnimatedDiv>
)
}
And the final component looks like this
class Survey extends Component {
constructor(props) {
super(props)
this.state = {
currentQuestionId: 1,
userAnswers: [],
isActive: false,
inputValue: '',
}
this.selectAnswer = this.selectAnswer.bind(this)
this.test = this.test.bind(this)
}
selectAnswer = answer => {
this.setState(state => ({
currentQuestionId: state.currentQuestionId + 1,
userAnswers: state.userAnswers.concat([answer]),
isActive: !state.isActive,
}))
}
checkInput = event => {
this.setState({
inputValue: event.target.value,
})
}
test = event => {
console.log(event.target.value)
}
render() {
const { currentQuestionId, isActive, inputValue } = this.state
const { questions } = this.props
const currentPercentage = (currentQuestionId * 100) / questions.length
return (
<SurveyContainer>
<Question
data={questions.find(q => q.id === currentQuestionId)}
className={isActive ? 'active' : ''}
handleClick={this.selectAnswer}
value={inputValue}
onChange={this.test}
/>
</SurveyContainer>
)
}
}
The InputField component renders out just fine, however, the function for my onChange event is not firing...There's a mistake somewhere in the pipeline, probably inside the question component?
It looks like you haven't passed any props to <InputField /> in your MultiChoiceQuestions component.
I can not see where you pass props from
<MultiChoiceQuestions>
...
<InputFiled props={props} />
...
</MultiChoiceQuestions>
Probably pass only the props which are needed in InputField component, such as inputValue, updateInputValue:
<InputFiled
inputValue={inputValue}
updateInputValue={updateInputValue}
/>
const InputField = (inputValue, updateInputValue) => {
...
<input
type="text"
placeholder="Please specify"
value={inputValue}
onChange={(e) => updateInputValue(e)}
/>
...
}
Hope that will help.

Using Dynamic Var with `Set` State in React Hooks?

This is a pretty common pattern in React components:
handleTextFieldChange(event)
{
const name = event.currentTarget.name;
this.setState({[name]: event.currentTarget.value})
}
What Javascript syntax could be used to do the same with React hooks?
i.e. something possibly along the lines of:
handleTextFieldChange(event)
{
const name = event.currentTarget.name;
this.set[name](event.currentTarget.value);
}
You could use a single useState with a default value of an object that contains all your input values and update that like you are used to with class components.
Example
const { useState } = React;
function App() {
const [state, setState] = useState({ email: "", password: "" });
function onChange(event) {
const { name, value } = event.target;
setState(prevState => ({ ...prevState, [name]: value }));
}
return (
<div>
<input value={state.email} name="email" onChange={onChange} />
<input value={state.password} name="password" onChange={onChange} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
How about something like this?
function handleTextFieldChange(mySetFunction, event) {
const value = event.currentTarget.value;
mySetFunction(value);
}
<TextField
placeholder="Email"
name="passwordResetEmailAddress"
onChange={(e) => handleTextFieldChange(setPasswordResetEmailAddress, e)}
>
{passwordResetEmailAddress}
</TextField>
I've tested it and it works.
class Yup extends React.Component {
state = {
first: "",
second: ""
};
handleTextFieldChange = ({ target: { name, value } }) =>
this.setState({ [name]: value });
render() {
const { first, second } = this.state;
return (
<div>
<p>{first}</p>
<p>{second}</p>
<input type="text" name="first" onChange={this.handleTextFieldChange} />
<input
type="text"
name="second"
onChange={this.handleTextFieldChange}
/>
</div>
);
}
}
same with hook
function Yup() {
const [{ first, second }, setState] = useState({ first: "", second: "" });
function handleTextFieldChange({ target: { name, value } }) {
setState(prevState => ({ ...prevState, [name]: value }));
}
return (
<div>
<p>{first}</p>
<p>{second}</p>
<input type="text" name="first" onChange={handleTextFieldChange} />
<input type="text" name="second" onChange={handleTextFieldChange} />
</div>
);
}
You can dynamically update a state for the target field by receiving an update state function as an argument in onChange function.
Example
import React, { useState } from "react";
const App = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const onChangeHandler = (setIdentifierState, event) => {
setIdentifierState(event.target.value);
};
return (
<div>
<p>{"Email: " + email}</p>
<p>{"Password: " + password}</p>
<input
type="text"
placeholder="email"
onChange={onChangeHandler.bind(null, setEmail)}
/>
<input
type="text"
placeholder="password"
onChange={onChangeHandler.bind(null, setPassword)}
/>
</div>
);
};
export default App;
I recently tackled this same problem while converting my components from classes to functions. I ended up creating an object that could then point to the separate state hooks:
const textStates = {};
const setTextStates= {};
for (var name of names) {
let [textState, setTextState] = useState("");
textStates[name] = textState;
setTextStates[name] = setTextState;
}
I solved it this way (a slightly more dynamic solution than what #VikR offered)
const [title, setTitle] = useState("");
const [desc, setDesc] = useState("");
const [video, setVideo] = useState("");
const handleChange = setter => event => {
const value = event.target.value;
//special cases
if (setter === setVideo) {
setInvalidVideo(!ReactPlayer.canPlay(value))
}
setter(value)
}
Then in my code:
<TextField fullWidth value={title} type="date"
label={t('service.ticket.add.title')}
placeholder={t('service.ticket.add.titlePh')}
onChange={handleChange(setTitle)} variant="outlined"/>

Categories

Resources