React Hooks only update state on form submit - javascript

So I've been at this all morning and can't figure out how to update my state correctly using useState.
I have a single controlled user input with name. When a user enters text and submits I would like to take the input value and push that to another state object namesList and map over that array in a child component.
Parent Component
import React, { Fragment, useState } from 'react';
import TextField from '#material-ui/core/TextField';
import NameInputList from './NameInputList';
const NameInputContainer = () => {
const [name, setName] = useState('');
const [namesList, setNamesList] = useState([]);
const handleChange = event => {
const { value } = event.target;
setName(value);
};
const handleSubmit = event => {
event.preventDefault();
setNamesList(prevState => [...prevState, name]);
setName('');
};
return (
<Fragment>
<form onSubmit={handleSubmit}>
<TextField
id="name"
label="Enter New Name"
variant="outlined"
value={name}
onChange={handleChange}
/>
</form>
{namesList.length > 0 && <NameInputList names={namesList} />}
</Fragment>
);
};
export default NameInputContainer;
Child Component
import React from 'react';
import PropTypes from 'prop-types';
const NameInputList = ({ names }) => {
console.log('child component names: ', names);
const generateKey = val => {
return `${val}_${new Date().getTime()}`;
};
return (
<ul>
{names.map((name, index) => ( // <--- Adding the index here seems to resolve the problem. I want to say the error was happening based on issues with having Unique keys.
<li key={generateKey(name + index)}>{name}</li>
))}
</ul>
);
};
NameInputList.propTypes = {
names: PropTypes.arrayOf(PropTypes.string)
};
NameInputList.defaultProps = {
names: []
};
export default NameInputList;
Seems like when I submit the first time the child component gets the correct value and renders as expected. When I go to input a new name there's a rerender on every handleChange. I'm not sure how to consistently: enter text > submit input > update namesList > render updated namesList in child component without handleChange breaking the functionality.

Just adding to that, as namesList prop is an array, a new copy will be sent for each parent re-render. Hence it's comparison will always be false and React will trigger a re-render of the child.
You can prevent the re-render by doing this :
export default React.memo(NameInputList, function(prevProps, nextProps) {
return prevProps.names.join("") === nextProps.names.join("");
});
This will ensure that NameInputList only re-renders when the contents of namesList actually change.

Related

Why setting the state through setTimeout doesn't render the correct props of a child component?

I have right here a component that should simply render a list of items. Also, the component includes an input that filters the list of items. If there is no items, or if the items are being loaded it should display a message.
import { useState } from "react";
export const List = ({ loading, options }) => {
const _options = options ?? [];
const [renderedOptions, setRenderedOptions] = useState(_options);
const [inputValue, setInputValue] = useState("");
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = _options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
In App.js, I did a setTimeout, to mock a fetch call. However, there is a problem. Although I'm setting the asyncOptions state to be the new list of items, in my <List /> component the options do not seem to display properly.
import { List } from "./List";
import { useState, useEffect } from "react";
const ITEMS = ["list_1", "list_2", "list_3", "list_4", "list_5"];
export default function App() {
const [asyncOptions, setAsyncOptions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const timeoutId = setTimeout(() => {
setIsLoading(false);
setAsyncOptions(ITEMS);
}, 2000);
return () => clearTimeout(timeoutId);
}, []);
return <List options={asyncOptions} loading={isLoading} />;
}
What is this happening and what is/are the solution(s)?
Sandbox code here: https://codesandbox.io/s/async-list-j97u32
The first time when list component gets rendered, renderedOptions is initialized with []
const [renderedOptions, setRenderedOptions] = useState(options);
But when the state inside the App component changes, it triggers a re-render and henceforth it triggers re-render of List Component. So since you are passing options as argument to useState u might feel it'll update the state automatically but that's not the case
Note -> useState doesn't take into consideration whatever you are passing as argument except for the first time the component loads
.So the useState will return back the initial State which is [] every time the component re-renders
So if you want to see changes you have to add useEffect inside the List component and trigger a state update every time options changes
Change your code too this,
import { useState } from "react";
export const List = ({ options, loading }) => {
console.log("Listt", options);
const [renderedOptions, setRenderedOptions] = useState([...options]);
const [inputValue, setInputValue] = useState("");
console.log(renderedOptions);
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
useEffect(() => {
setRenderedOptions(options)
} , [options])
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
Basically, in the beginning, the value of options in an empty array, and the value put in state is a copy of that so the component is not listening to changes on the prop.
For some reason, you have to use the useEffect hook to actively listen to changes in the prop. By using the hook, when the API call returns something, it will set the state.(BTW, if anyone knows what is going on tell us)
I would recommend moving the API call to the List component, it would better encapsulate the logic
import { useEffect, useState } from "react";
export const List = ({ loading, options }) => {
const [renderedOptions, setRenderedOptions] = useState(options);
const [inputValue, setInputValue] = useState("");
useEffect(() => {
setRenderedOptions(options);
}, [options]);
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};

React custom input passing props to parent functional component while using custom hooks

Am new to react and i have a custom input where am handling the value and input handler via a custom hook but would like to get the value and the input handler to the parent component using the custom input but am stuck on how to achieve this.
I have written the following code.
On the custom hook
import {useReducer} from "react";
const INITAL_STATE = {value:'',valid:false,pristine:true, error:''}
const REDUCER_ACTIONS = { input:"INPUT", blur:"BLUR"}
const reducer = (state,action)=>{
if (action.type === REDUCER_ACTIONS.input){
return {...state, value: action.value}
}
if (action.type === REDUCER_ACTIONS.blur){
return {...state, pristine: false}
}
return INITAL_STATE;
}
const useForm = () => {
const [inputState, dispatch] = useReducer(reducer,INITAL_STATE)
const onBlurHandler = (event) => {
dispatch({type:REDUCER_ACTIONS.blur});
}
const onInputHandler = (event) => {
dispatch({type:REDUCER_ACTIONS.input,value:event.target.value})
}
return {
...inputState,
onBlurHandler,
onInputHandler
}
};
export default useForm;
And for my custom input i have
import useForm from "../../hooks/use-form";
const CustomInput = (props) => {
const {value, onInputHandler, onBlurHandler} = useForm(); //uses custom hook
return <>
<label htmlFor={props.id}>{props.label}</label>
<input value={value} onBlur={onBlurHandler} onInput={onInputHandler}
{...props} />
</>
}
export default CustomInput;
The above custom input has the onInput and onBlur pointing to the custom hooks since i want to reuse the functionality on other input types like select and date pickers without having to duplicate them.
On my parent component am simply calling the Custom input like
function App() {
return (
<div className="container">
<CustomInput onInputHandler={} label="First name"/>
</div>
);
}
export default App;
I would like to pass the onInputHandler and value as a props back to the parent component from the custom input but am stuck on how to do this. How do i proceed?
When you say you need to pass value, I guess you wanted to pass the initial value of the input to CustomInput. To achieve that you can pass another prop.
App.js pass initialValue to CustomInput
<CustomInput
initialValue={"abc"}
label="First name"
/>
In CustomInput pass initialValue prop to useForm hook as an argument.
const { value, onInputHandler, onBlurHandler } = useForm(props.initialValue);
Set the initialValue as the value in initial state in useForm.
const useForm = (initialValue) => {
const [inputState, dispatch] = useReducer(reducer, {
...INITAL_STATE,
value: initialValue
});
...
...
}
To pass the onInputHandler as a prop you can check if onInputHandler is available as a prop and call it along with onInputHandler coming from useForm.
In App.js defines another function that accepts event as an argument.
export default function App() {
const onInputHandler = (e) => {
console.log(e);
};
return (
<div className="App">
<CustomInput
...
onInputHandler={onInputHandler}
label="First name"
/>
</div>
);
}
In CustomInput change the onInput handler like below. You can change the logic as per your needs (I called onInputHandler in useForm and prop).
<input
value={value}
onBlur={onBlurHandler}
onInput={(e) => {
props.onInputHandler && props.onInputHandler(e);
onInputHandler(e);
}}
{...props}
/>
My approach to this will be to simply call the onInputHandler() from hooks and onInputHandler() from the props received from Parent and send the e.target.value as a prop to these functions.
const CustomInput = (props) => {
const { value, onInputHandler, onBlurHandler } = useForm(); //uses custom hook
console.log(value);
const handleInputChange = (e: any) => {
onInputHandler(e);
props.onInputHandler(e.target.value);
};
return (
<>
<label htmlFor={props.id}>{props.label}</label>
<input
value={value}
onBlur={onBlurHandler}
onInput={(e) => {
handleInputChange(e);
}}
{...props}
/>
</>
);
};
export default CustomInput;
And in the parent component we can receive them as props returned from that function and use it according to our requirement.
function App() {
return (
<div className="container">
<CustomInput
label="name"
onInputHandler={(value: string) => console.log("App",value)}
/>
</div>
);
}
export default App;
sandbox link : https://codesandbox.io/s/nifty-lamport-2czb8?file=/src/App.tsx:228-339

updating the Child Component based on search results in react-hooks functional components

I have react-App with redux using react-hooks functional components, it renders the list of tutors in child component correctly. i have added a search box in it and handlchange function to update the list component(child compenent). the handlechange function works fine and assign the newlist to filtered variable in side the handlechange function body however the value of filtered outside the handlechange function remains unchanged.so the updated filtered variable not assigned to TutorList Component.
the Tutors.jsx Component is as follows:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams, useHistory } from 'react-router-dom';
import TutorList from "./TutorList";
import * as actions from "../_actions/tutorActions";
import { TextField, Button, FormControl, } from "#material-ui/core";
const initialFieldValues = {
search: ""
}
const Tutors = (props) => {
const [values, setValues] = useState(initialFieldValues)
let history = useHistory()
const dispatch = useDispatch();
// getting of tutorlist
let tutor = useSelector(state => state.tutor.list);
// sorting of tutors on date
let tutorList = tutor.sort((a, b) => b.updatedOn.localeCompare(a.updatedOn));
useEffect(() => {
dispatch(actions.fetchAll())
}, [])
console.log("tutorList:", tutorList)
// Variable to hold the filtered list before putting into state
let newList = [];
let filtered = tutorList;
//when filter changes from '' to something filtered should be updated with newlist
function handleChange(e) {
const { name, value } = e.target
const fieldValue = { [name]: value }
setValues({
...values,
...fieldValue
})
// If the search bar isn't empty
if (values.search !== "") {
// Use .filter() to determine which items should be displayed
// based on the search terms
newList = tutorList.filter(item => {
// change current item to lowercase
const lc = item.fullName.toLowerCase();
// change search term to lowercase
const filter = e.target.value.toLowerCase();
console.log("filter", filter);
// check to see if the current list item includes the search term
// If it does, it will be added to newList. Using lowercase eliminates
// issues with capitalization in search terms and search content
return lc.includes(filter);
});
} else {
newList = tutorList;
}
console.log("newList:", newList)//shows correct list
filtered = newList
console.log("filtered:", filtered)//shows correct value
}
return (
<div>
<br />
<TextField
name="search"
variant="outlined"
label="Search Tutor"
paceholder="search tutor..."
value={values.search}
onChange={handleChange}
/>
<TutorList
tutorList={filtered}
/>
<Button onClick={() => history.goBack()}
size="small" variant="contained" color="secondary">
back
</Button>
</div>
);
}
export default Tutors;
the TutorList Component should Show the newList based on filter entered in search field. any one help to workaround or better solution .thanks in advance.
There's no need to update newList, filtered from handleChange. React doesn't work this way. You won't get a rerender. The only thing you should do in your handleChange is setValue
const [textInputValue, setTextInputValue] = useState('');
function handleChange(e) {
setTextInputValue(e.target.value);
}
then do all the sorting based on
let tutor = useSelector(state => state.tutor.list)
// and textInputValue
Also there's no need to store filtered values in state.
create a new state for
const Tutors =(props)=> {
const [values, setValues] = useState(initialFieldValues)
const [filtered, setFiltered] = useState(null)
inside of handleChange() function set the filtered with that of filtered element:
function handleChange(e){
//........
console.log("newList:",newList)//shows correct list
//filtered=newList <= instead of this
setFiltered(newList) // <= do this
console.log("filtered:",filtered)//shows correct value
}
The final code should look like this:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams, useHistory } from "react-router-dom";
import TutorList from "./TutorList";
import * as actions from "../_actions/tutorActions";
import { TextField, Button, FormControl } from "#material-ui/core";
const initialFieldValues = {
search: "",
};
const Tutors = (props) => {
const [values, setValues] = useState(initialFieldValues);
const [filtered, setFiltered] = useState(null);
let history = useHistory();
const dispatch = useDispatch();
// getting of tutorlist
let tutor = useSelector((state) => state.tutor.list);
// sorting of tutors on date
let tutorList = tutor.sort((a, b) => b.updatedOn.localeCompare(a.updatedOn));
useEffect(() => {
dispatch(actions.fetchAll());
}, []);
console.log("tutorList:", tutorList);
// Variable to hold the filtered list before putting into state
let newList = [];
let filtered = tutorList;
//when filter changes from '' to something filtered should be updated with newlist
function handleChange(e) {
const { name, value } = e.target;
const fieldValue = { [name]: value };
setValues({
...values,
...fieldValue,
});
// If the search bar isn't empty
if (values.search !== "") {
// Use .filter() to determine which items should be displayed
// based on the search terms
newList = tutorList.filter((item) => {
// change current item to lowercase
const lc = item.fullName.toLowerCase();
// change search term to lowercase
const filter = e.target.value.toLowerCase();
console.log("filter", filter);
// check to see if the current list item includes the search term
// If it does, it will be added to newList. Using lowercase eliminates
// issues with capitalization in search terms and search content
return lc.includes(filter);
});
} else {
newList = tutorList;
}
console.log("newList:", newList); //shows correct list
setFiltered(newList); // update filtered state
console.log("filtered:", filtered); //shows correct value
}
return (
<div>
<br />
<TextField
name="search"
variant="outlined"
label="Search Tutor"
paceholder="search tutor..."
value={values.search}
onChange={handleChange}
/>
{filtered && <TutorList tutorList={filtered} />}
<Button
onClick={() => history.goBack()}
size="small"
variant="contained"
color="secondary"
>
back
</Button>
</div>
);
};
export default Tutors;

Updating initialValues prop on Formik Form does not update input value

I use a formik form with forward refs like so
Form.js
import React from "react";
import FormikWithRef from "./FormikWithRef";
const Form = ({
formRef,
children,
initialValues,
validationSchema,
onSubmit
}) => {
return (
<FormikWithRef
validateOnChange={true}
validateOnBlur={true}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
ref={formRef}
>
{(props) => <form onSubmit={props.handleSubmit}>{children}</form>}
</FormikWithRef>
);
};
export default Form;
FormikWithRef.js
import React, { forwardRef, useImperativeHandle } from "react";
import { Formik } from "formik";
function FormikWithRef(props, ref) {
let _formikProps = {};
useImperativeHandle(ref, () => _formikProps);
return (
<Formik {...props}>
{(formikProps) => {
_formikProps = formikProps;
if (typeof props.children === "function") {
return props.children(formikProps);
}
return props.children;
}}
</Formik>
);
}
export default forwardRef(FormikWithRef);
I have some tabs, that update an easy-peasy store state type, when I select the 2nd tab, I was wanting to update the input value (that initially comes from the store state of value) with a Formik form, but updating state initialValues specific to that component that gets passed as initialValues prop to the Formik component.
TabsForm.js
import React, { useState, useEffect, useRef } from "react";
import styled from "styled-components";
import { useStoreState } from "easy-peasy";
import Form from "./Form";
import MoneyBox from "./MoneyBox";
const Container = styled.div`
width: 100%;
background-color: #dfdfdf;
`;
const FieldWrapper = styled.div`
padding: 20px 12px;
`;
const TabsForm = () => {
const [initialValues, setInitialValues] = useState();
const type = useStoreState((state) => state.type);
const value = useStoreState((state) => state.value);
const formRef = useRef(null);
const onFormSubmit = async (values) => {
console.log({ values });
};
useEffect(() => {
if (value && type) {
let filterVal = { ...value };
/* here is where I update the input value to be 3000,
the initial values get updated and in the `Form.js` file,
the console log from here also reflects this update,
however, the input field does not update? */
if (type === "Two") filterVal.input = 30000;
setInitialValues(filterVal);
}
}, [value, type]);
useEffect(() => {
// check initialValues has updated
console.log({ initialValues });
}, [initialValues]);
return (
<Container>
{initialValues && type ? (
<Form
initialValues={initialValues}
onSubmit={onFormSubmit}
formRef={formRef}
>
<FieldWrapper>
<MoneyBox name="input" currencySymbol={"£"} />
</FieldWrapper>
</Form>
) : null}
</Container>
);
};
export default TabsForm;
When clicking the 2nd tab;
The initialValues state in TabsForms.js updates so that value.input = 30000;
The initialValues prop in both Form.js and FormikWithRef.js also reflect that value.input = 3000
However, the input does not update, using the useField hook from forimk in the MoneyBox.js component, the field object does not have a value of 30000, instead it's whatever the field value was before, why is this?
I have created a CodeSandbox to see all the components used, and console logs to see that the Formik does receive the updated value, but doesn't seem to apply it.
I've been stuck on this for a few days and can't seem to find a solution, any help would be greatly appreciated.
If you want the value of the input to change when you change initialValues, you need to pass to the Formik component the prop enableReinitialize as true.
So, what you need to change in your code is in TabsForm.js pass to your Form component the prop enableReinitialize
<Form
enableReinitialize
initialValues={initialValues}
onSubmit={onFormSubmit}
formRef={formRef}
>
<FieldWrapper>
<MoneyBox name="input" currencySymbol={"£"} />
</FieldWrapper>
</Form>
And in your Form.js pass that prop to the Formik component
const Form = ({
formRef,
children,
initialValues,
validationSchema,
onSubmit,
enableReinitialize
}) => {
return (
<FormikWithRef
enableReinitialize={enableReinitialize}
validateOnChange={true}
validateOnBlur={true}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
ref={formRef}
>
{(props) => <form onSubmit={props.handleSubmit}>{children}</form>}
</FormikWithRef>
);
};
I'm not quite sure how you business logic should work, but here is a working example with the changes above.
( Solution CodeSandbox)
It's called initialValues, so why you expect it to update the form values when you change it? (however you can ask it to do so by using enableReinitialize prop, as #Vencovsky mentioned in another answer.)
For binding your desired value (value.input in the easy-peasy store) to formik input, you can use:
const [field, meta, helpers] = useField(props);
useEffect(() => {
helpers.setValue(value.input)
}, [value])
it will update the value of the formik input field every time the value in store changes.
and for changing the value of the state in store, you can use the way you did it for setting tabs. (using easy-peasy store.)
Run It On CodeSandbox
On line 49 in Tabs.js, it updates the value when a tab is clicked.
On line 19 in Input.js, it binds the input value to your store state.
enableReinitialize={true} prop will do the trick for you

Replace parent state with state of child. Button in parent

EDIT: See the comment of O.o for the explanation of the answer and the variant in case you are using classes.
I've come across to something and I can't find the solution.
I have 4 components in my web app:
Parent
child_1
child_2
child_3
I have a button on the Parent, and different forms (with inputs, checkboxes and radiobuttons) at the children.
Each child has his own button that executes several functions, some calculations, and updates the corresponding states. (No states are passed through parent and child).
I need to replace the three buttons of the children with the parent button.
Is there a way that I can execute the functions at the three children from the parent button and retrieve the results? (the results are one state:value per child.)
function Child1(props) {
const [value, setValue] = useState("");
useEffect(() => {
calculate();
}, [props.flag]);
calculate() {
//blah blah
}
onChange(e) {
setValue(e.target.value);
props.onChange(e.target.value); // update the state in the parent component
}
return (
<input value={value} onChange={(e) => onChange(e)} />
);
}
function Parent(props) {
const [flag, setFlag] = useState(false);
const [child1Value, setChild1Value] = useState("");
return (
<div>
<Child1 flag={flag} onChange={(value) => setChild1Value(value)}/>
<button onClick={() => setFlag(!flag)} />
</div>
);
}
I didn't test this but hope this helps you. And lemme know if there is an issue.
Try the following:
create refs using useRef for child form components.
for functional components, in order for the parent to access the child's methods, you need to use forwardRef
using the ref, call child component functions on click of parent submit button (using ref.current.methodName)
See the example code. I have tested it on my local, it is working ok.
Parent
import React, { Fragment, useState, useRef } from "react";
import ChildForm1 from "./ChildForm1";
const Parent = props => {
const [form1Data, setFormData] = useState({});//use your own data structure..
const child1Ref = useRef();
// const child2Ref = useRef(); // for 2nd Child Form...
const submitHandler = e => {
e.preventDefault();
// execute childForm1's function
child1Ref.current.someCalculations();
// execute childForm2's function
// finally do whatever you want with formData
console.log("form submitted");
};
const notifyCalcResult = (calcResult) => {
// update state based on calcResult
console.log('calcResult', calcResult);
};
const handleChildFormChange = data => {
setFormData(prev => ({ ...prev, ...data }));
};
return (
<Fragment>
<h1 className="large text-primary">Parent Child demo</h1>
<div>
<ChildForm1
notifyCalcResult={notifyCalcResult}
ref={child1Ref}
handleChange={handleChildFormChange} />
{/*{do the same for ChildForm2 and so on...}*/}
<button onClick={submitHandler}>Final Submit</button>
</div>
</Fragment>
);
};
export default Parent;
ChildFormComponent
import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react";
const ChildForm1 = ({ handleChange, notifyCalcResult }, ref) => {
const [name, setName] = useState("");
const [calcResult, setCalcResult] = useState([]);
const someCalculations = () => {
let result = ["lot_of_data"];
// major calculations goes here..
// result = doMajorCalc();
setCalcResult(result);
};
useImperativeHandle(ref, () => ({ someCalculations }));
useEffect(() => {
// notifiy parent
notifyCalcResult(calcResult);
}, [calcResult]);
return (
<form className="form">
<div className="form-group">
<input
value={name}// //TODO: handle this...
onChange={() => handleChange(name)}//TODO: notify the value back to parent
type="text"
placeholder="Enter Name"
/>
</div>
</form>
);
};
export default forwardRef(ChildForm1);
Also as a best practice, consider to maintain state and functions in the parent component as much as possible and pass the required values/methods to the child as props.

Categories