Component gets re-initialized on props update - reactjs

I have a component inside formik -based step wizard.
I make use of useField, useEffect and useState hooks as below:
const [counts, countsMeta, countsHelpers] = useField('counts');
const [visible, setVisible] = useState(false);
useEffect(() => {
countsHelpers.setValue(
Array(event.book && event.book.length ? event.book.length : 0).fill(0)
);
// eslint-disable-next-line
}, [event.book]);
I understand that my useEffect should only be called once, no matter what as long as event.book never changes.
I also have a way to apply coupons as below:
<div className={styles.coupon}>
<FontAwesomeIcon
icon={faPlusSquare}
onClick={() => setVisible(!visible)}
/>{' '}
coupon code
{visible && (
<div>
<input
id='coupon'
type='text'
value={values.coupon || ''}
onChange={onChange}
placeholder='Coupon code'
className={errors.coupon ? styles.error : ''}
/>
<div
className={styles.apply}
onClick={() => loadPromo(values.coupon)}
>
Apply
</div>
</div>
)}
</div>
For some reason I cannot pinpoint, when I call loadPromo() my component re-initializes. Once it happens, state and fields are reset e.g. visible is again set to false and counts array reinitialized with zeros. After that, everything is working fine.
What am I missing?

Related

Set item in onclick is not logging expected output

I'm writing a simple react code that adds a value to a list onClick of a button. and after adding, I'm logging it in the same block. Currently, my issue is, that the logging is happening with n-1 entered string. i.e. If I enter egg and then add milk, after adding milk, I see egg logged and so on. Here is my code.
function App() {
const [list, setList] = useState([]);
const [gItem, setGItem] = useState("");
const AddItem = (e) => {
e.preventDefault();
setList([...list, gItem]);
console.log(list);
};
return (
<>
<form className="grocery-form">
<h3>grocery bud</h3>
<div className="form-control">
<label htmlFor="name"></label>
<input
type="text"
placeholder="e.g. eggs"
className="grocery"
name="name"
id="name"
onChange={(e) => setGItem(e.target.value)}
/>
<button className="submit-btn" type="submit" onClick={AddItem}>
Submit
</button>
</div>
</form>
<div className="grocery-container">
<List items={list} />
</div>
</>
);
}
I'm unable to understand where I'm going wrong.
setList updates state asynchronously so if you log state after using it the previous value will be displayed, to make it log the current state after this list was changed you can use useEffect hook like this:
useEffect(() => {
console.log(list);
}, [list])

React re renders everything every time key is pressed

I have a chart and an input field below. Basically the user can ask a question and the chart will change accordingly. Here is how it looks like
Here is the sample of the code below (ignore CoreUI syntaxes)
<CRow>
<CCol xs="12" sm="12" lg="12">
<CCard id="answerScreen" style={{height: "500px"}}>
{
!tempResponse.loading? tempResponse.data.map(value => (
<ChartRender
key={uuidv4()}
data={value}
/>
))
:
<Loader/>
}
</CCard>
</CCol>
</CRow>
<CRow>
<CCol xs="12" sm="12" lg="12">
<CCard>
<CCardBody>
<CForm className="form-horizontal">
<CFormGroup>
<CInputGroup size="lg" className="input-prepend">
<CInputGroupPrepend>
<CInputGroupText className="">#Ask</CInputGroupText>
</CInputGroupPrepend>
<CInput
size="16"
type="text"
value={userInput || ""}
onChange={e=> handleTextBoxInput(e)}
onClick={e=> handleTextBoxClick(e)}
onKeyPress={e => handleTextBoxEnter(e)}
id="userQuery"
/>
<CInputGroupAppend>
<CButton color="primary">Send</CButton>
</CInputGroupAppend>
</CInputGroup>
</CFormGroup>
</CForm>
</CCardBody>
</CCard>
</CCol>
</CRow>
This is how I have defined my states
const [userInput, setUserInput] = React.useState("");
const [tempResponse, setTempResponse] = React.useState({
data: []
})
I suspect the problem in this part of the code
<CInput
size="16"
type="text"
value={userInput || ""}
onChange={e=> handleTextBoxInput(e)}
onClick={e=> handleTextBoxClick(e)}
onKeyPress={e => handleTextBoxEnter(e)}
id="userQuery"
/>
I even tried adding useCallback to onChange function like this
const handleTextBoxInput = useCallback(e =>{
e.preventDefault();
setUserInput(e.target.value)
}, [])
But no help. I even read about memo but not sure where or how to apply it in my situation. What am I doing wrong?
OBSERVATION
As mentioned by #Matthew ,arrow syntax creates a different callback each time which contributes to re rendering and hence must be removed.
But even after removing it, the chart is getting re rendered every time a key is pressed. I am using Chartjs which uses canvas internally. Is Chartjs a problem?
You are correct about the problemetic code.
<CInput
size="16"
type="text"
value={userInput || ""}
onChange={e=> handleTextBoxInput(e)} // performance issue
onClick={e=> handleTextBoxClick(e)} // performance issue
onKeyPress={e => handleTextBoxEnter(e)} // performance issue
id="userQuery"
/>
When the above code is run multiple times, the functions will be re-created every time. So instead of doing that, the following is enough
<CInput
size="16"
type="text"
value={userInput || ""}
onChange={handleTextBoxInput}
onClick={handleTextBoxClick}
onKeyPress={handleTextBoxEnter}
id="userQuery"
/>
The useCallback hook returns, well, a callback function. You can simply use the return values as normal event callbacks.
On your input, you have two events firing on every keypress - onKeyPress and onChange - remove the onKeyPress.
I suspect that handleTextBoxEnter calls setTempResponse which updates tempResponse. Setting state that the UI depends on will trigger a re-render.
You should also handle form submissions using the onSubmit event. If an element has focus inside of a form and the enter button is pressed - it will fire onSubmit.
<form onSubmit={handleTextBoxEnter}></form>
Also, if a key changes React will re-render. You are calling a function in your key so it is updated on every update.
tempResponse.data.map((value, i) => (
<ChartRender key={`chart-${i}`} data={value} />
))
FYI manually creating a UUID for a key overkill.
export const YourComponent = (): JSX.Element => {
const [userInput, setUserInput] = useState('');
const [tempResponse, setTempResponse] = useState({ data: [], loading: true });
useEffect(()=>{
// handle initial data loading and set loading to false
}, [])
const handleSubmit = (e) => {
e.preventDefault();
setTempResponse(your_state_data);
};
// e.preventDefault shouldn't be used here and is not required
const handleChange = ({ target }) => setUserInput(target.value);
if (tempResponse.loading) {
return <Loading />;
}
// action is set to # for iOS - an action is required to show the virtual submit button on the keyboard
return (
<>
<form action="#" onSubmit={handleSubmit}>
<input defaultValue={userInput} onChange={handleChange} type="text" />
<button type="submit">Submit</button>
</form>
{!!tempResponse.length &&
tempResponse.data.map((value, i) => (
<ChartRender key={`chart-${i}`} data={value} />
))}
</>
);
};

In React with Formik how can I build a search bar that will detect input value to render the buttons?

New to Formik and React I've built a search component that I'm having issues with the passing of the input value and rendering the buttons based on input length.
Given the component:
const SearchForm = ({ index, store }) => {
const [input, setInput] = useState('')
const [disable, setDisable] = useState(true)
const [query, setQuery] = useState(null)
const results = useLunr(query, index, store)
const renderResult = results.length > 0 || query !== null ? true : false
useEffect(() => {
if (input.length >= 3) setDisable(false)
console.log('input detected', input)
}, [input])
const onReset = e => {
setInput('')
setDisable(true)
}
return (
<>
<Formik
initialValues={{ query: '' }}
onSubmit={(values, { setSubmitting }) => {
setInput('')
setDisable(true)
setQuery(values.query)
setSubmitting(false)
}}
>
<Form className="mb-5">
<div className="form-group has-feedback has-clear">
<Field
className="form-control"
name="query"
placeholder="Search . . . . ."
onChange={e => setInput(e.currentTarget.value)}
value={input}
/>
</div>
<div className="row">
<div className="col-12">
<div className="text-right">
<button type="submit" className="btn btn-primary mr-1" disabled={disable}>
Submit
</button>
<button
type="reset"
className="btn btn-primary"
value="Reset"
disabled={disable}
onClick={onReset}
>
<IoClose />
</button>
</div>
</div>
</div>
</Form>
</Formik>
{renderResult && <SearchResults query={query} posts={results} />}
</>
)
}
I've isolated where my issue is but having difficulty trying to resolve:
<Field
className="form-control"
name="query"
placeholder="Search . . . . ."
onChange={e => setInput(e.currentTarget.value)}
value={input}
/>
From within the Field's onChange and value are my problem. If I have everything as posted on submit the passed query doesn't exist. If I remove both and hard code a true for the submit button my query works.
Research
Custom change handlers with inputs inside Formik
Issue with values Formik
Why is OnChange not working when used in Formik?
In Formik how can I build a search bar that will detect input value to render the buttons?
You need to tap into the props that are available as part of the Formik component. Their docs show a simple example that is similar to what you'll need:
<Formik
initialValues={{ query: '' }}
onSubmit={(values, { setSubmitting }) => {
setInput('')
otherStuff()
}}
>
{formikProps => (
<Form className="mb-5">
<div className="form-group has-feedback has-clear">
<Field
name="query"
onChange={formikProps.handleChange}
value={formikProps.values.query}
/>
</div>
<button
type="submit"
disabled={!formikProps.values.query}
>
Submit
</button>
<button
type="reset"
disabled={!formikProps.values.query}
onClick={formikProps.resetForm}
>
</Form>
{/* ... more stuff ... */}
)}
</Formik>
You use this render props pattern to pull formiks props out (I usually call them formikProps, but you can call them anything you want), which then has access to everything you need. Rather than having your input, setInput, disable, and setDisable variables, you can just reference what is in your formikProps. For example, if you want to disable the submit button, you can just say disable={!formikProps.values.query}, meaning if the query value in the form is an empty string, you can't submit the form.
As far as onChange, as long as you give a field the correct name as it corresponds to the property in your initialValues object, formikProps.handleChange will know how to properly update that value for you. Use formikProps.values.whatever for the value of the field, an your component will read those updates automatically. The combo of name, value, and onChange, all handled through formikProps, makes form handing easy.
Formik has tons of very useful prebuilt functionality to handle this for you. I recommend hanging out on their docs site and you'll see how little of your own code you have to write to handle these common form behaviors.

onClick in reactjs not showing items

I have to show a list of defaultValues in the search list and when I click on any of those item then it should take me to that item's component but it's not going anywhere. It's only happening with the defaultValues because as soon as I start typing, then if I click on any search result then it takes me to the desired component. what is wrong with my code?
here's the code
const [search, setSearch] = useState("");
const [showDefaultValues, setShowDefaultValues] = useState(false);
const [defaultValues] = useState({
Mumbai: true
});
{!search.length && showDefaultValues ? (
<div className="result-box">
{data
.filter((item, idx) => defaultValues[item.district])
.map((dist, idx) => (
<div
key={idx}
className="search-result"
onClick={() => {
onResultClick(dist.district);
}}
>
{highlightedText(dist.district, search)}
</div>
))}
</div>
) : null}
Just change the codes at components/search/search.js line 39 to 49
<input
placeholder="Search for a District..."
type="text"
className="search-input"
value={search}
onChange={onSearchInputChange}
onFocus={() => {
toggleDefaultValues(true);
}}
onBlur={onBlurInput}
/>
To
<input
placeholder="Search for a District..."
type="text"
className="search-input"
value={search}
onChange={onSearchInputChange}
onFocus={() => {
toggleDefaultValues(true);
}}
/>
Or simply remove line 48
To compensate this, you can add below inside your useEffect (similar to componentDidMount)
document.addEventListener("mousedown", handleInputClickOutside);
add function handleInputClickOutside to set the state to false/hide
You forgot to implement the onClick logic on the default search result items and that's why the search results work fine, while the default search items do not.
Check this link to the working codesandbox.
All i did was invoke the same onResultClick function onClick of 'District' component.
<div
className="dist"
onClick={() => {
this.props.onResultClick(item.district);
}}
>
...
</div>
Hope this solves your problem.

React component second time render issue

I am building a tasks board app and I have an issue with rendering the TasksList component within a board of 3 lists: 1 Board -> 3 lists -> N tasks
It seems like the TasksList component is being rendered twice, which is fine, but on the 2nd time it seems to return different values for each task (which are wrong according to my conditional return, and right on the first render - why would there be a difference?)
I also get this warning. Maybe ts related:
Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
Board.js component render function:
const tasksListsArr = Object.entries(this.state.tasks).map(list => {
return (
<TasksList
key={list[0]}
listrole={list[0]}
listTitle={this.state.Lists[list[0]]}
tasks={list[1]}
listTextChangedHandler={event => this.listTextChangedHandler(list[0], event)}
addTaskHandler={() => this.addTaskHandler(list[0])}
taskDeleteHandler={this.taskDeleteHandler}
moveTaskHandler={this.moveTaskHandler}
taskEditHandler={this.taskEditHandler}
taskEditApprove={this.taskEditApprove}
/>
)
})
TaskList.js component:
import React from "react"
import classes from "./TasksList.module.css"
const TasksList = props => {
const tasks = props.tasks.map(task => {
const buttonLeft =
!task.isEdit && (props.listrole === "inprogress" || props.listrole === "done") ? (
<button onClick={() => props.moveTaskHandler(task.id, "left")}>left</button>
) : null
const buttonRight =
!task.isEdit && (props.listrole === "inprogress" || props.listrole === "backlog") ? (
<button onClick={() => props.moveTaskHandler(task.id, "right")}>right</button>
) : null
const taskUtils = task.isEdit ? null : (
<div>
<span onClick={() => props.taskDeleteHandler(props.listrole, task.id)}>X</span>
<span onClick={() => props.taskEditHandler(props.listrole, task.id)}>edit</span>
</div>
)
const taskContent = task.isEdit ? (
<div>
<input
type='text'
onChange={event => props.listTextChangedHandler(props.listrole, event)}
/>
<button onClick={props.taskEditApprove(props.listrole, task.id)}>OK</button>
</div>
) : (
<div>
<div>{task.text}</div>
</div>
)
return (
<div key={task.id} className={classes.Task}>
{buttonLeft}
{taskContent}
{buttonRight}
{taskUtils}
</div>
)
})
console.log(tasks)
return (
<div className={classes.List}>
<h2 className={classes.ListTitle}> {props.listTitle} </h2>
<input type='text' onChange={props.listTextChangedHandler} placeholder='Add task...' />
<button onClick={props.addTaskHandler}>+</button>
<div className={classes.TasksList}>{tasks}</div>
</div>
)
}
export default TasksList
I suspect the issue is in TaskList component. Because the onChange of input and onClick of button getting called on every render but those event handler functions should be called only when user integrated with it. So to fix change them to arrow way so that the function gets called only when we interact.
Following changes required in TaskList.js
Change
<button onClick={props.taskEditApprove(props.listrole, task.id)}>OK</button>
To
<button onClick={() => props.taskEditApprove(props.listrole, task.id)}>OK</button>
And
Change
<input type='text' onChange={props.listTextChangedHandler} placeholder='Add task...' />
<button onClick={props.addTaskHandler}>+. </button>
To
<input type='text' onChange={event => props.listTextChangedHandler(event)} placeholder='Add task...' />
<button onClick={() => props.addTaskHandler()}>+</button>
OK
props.taskEditApprove is being called in render.
Try
props.taskEditApprove(props.listrole, task.id)}>OK
Then function will be called only on interaction.

Resources