In my code, I did not use constructor (). I've always seen people use the constructor in class components, but even though I'm not using it in that code, it's working perfectly. In my code, putting the state outside the constructor, is it a good idea or would it be better to use the constructor with the state set inside it? Can it give some sort of error in the future, or worsen my system's performance doing so? What is more advisable to do in this case?
import React, { Component, Fragment } from 'react'
import {Redirect} from 'react-router-dom'
import { connect } from 'react-redux'
import ActionCreator from '../redux/actionCreators'
import Button from '../elements/Button'
const statsgenre = {
'Ação': 'Action',
'Comédia': 'Comedy',
'Drama': 'Drama'
}
const statsuser = {
'Assistido' : 'Watched',
'Assistindo': 'Watching',
'Assistir': 'Watch'
}
class ScreensEditSeries extends Component{
state = {
id: '',
name: '',
status: '',
genre: '',
notes: ''
}
componentDidMount = () => {
const serie = {...this.props.match.params}
this.props.load(serie)
this.props.reset()
}
static getDerivedStateFromProps(newProps, prevState){
let serie = {}
if (prevState.name === '' || prevState.name === undefined){
if (newProps.series.serie.name !== prevState.name){
serie.name = newProps.series.serie.name
}
if (newProps.series.serie.genre !== prevState.genre){
serie.genre = newProps.series.serie.genre
}
if (newProps.series.serie.status !== prevState.status){
serie.status = newProps.series.serie.status
}
if (newProps.series.serie.notes !== prevState.notes){
serie.notes = newProps.series.serie.notes
}
return serie
}
}
saveSeries = () => {
const {name, status, genre, notes} = this.state
const id = this.props.match.params.id
const newSerie = {
id,
name,
status,
genre,
notes
}
this.props.save(newSerie)
}
handleChange = field => event => {
this.setState({[field] : event.target.value})
}
render(){
return (
<Fragment>
<div className="container">
<div>
{this.props.series.saved && <Redirect to={`/series/${this.props.match.params.genre}`}/>}
<h1 className='text-white'>Edit Série</h1>
{!this.props.series.isLoadding && <Button>
Name: <input type="text" value={this.state.name} onChange={this.handleChange('name')} className="form-control" /><br />
Status: {<span> </span>}
<select value={this.state.status} onChange={this.handleChange('status')}>
{Object.keys(statsuser)
.map( key => <option key={key}>{statsuser[key]}</option>)}
</select><br/><br/>
Genre: {<span> </span>}
<select value={this.state.genre} onChange={this.handleChange('genre')}>
{Object.keys(statsgenre)
.map(key => <option key={key}>{statsgenre[key]}</option>)}
</select><br/><br/>
Notes: <textarea type='text' value={this.state.notes} onChange={this.handleChange('notes')} className="form-control"></textarea><br />
<button className="button button2" type="button" onClick={this.saveSeries}>Save</button>
</Button>}
{this.props.series.isLoadding && <p className='text-info'>Loading...</p>}
</div>
</div>
</Fragment>
)
}
}
const mapStateToProps = state => {
return {
series: state.series
}
}
const mapDispatchToProps = dispatch => {
return {
load : serie => dispatch(ActionCreator.getSerieRequest(serie)),
save: newSerie => dispatch(ActionCreator.updateSerieRequest(newSerie)),
reset : () => dispatch(ActionCreator.seriesReset()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ScreensEditSeries)
In general, you should only use a constructor if you need logic when the class is first created, or if your setup depends on the props passed in. Since everything in your initial state is hardcoded not using a constructor is fine in this case.
There is no problem in using class components without a constructor. Usually you need it in case you have to do some work to prepare the state, process some props or other setup some instance variables as soon as the component is instantiated.
It's ok :)
Here, instead, there is a very interesting post from Dan Abramov about why, if you need to use the constructor, is needed to call super(props):
https://overreacted.io/why-do-we-write-super-props/
Not super related to the question, but asking about constructor, I thought it could be useful to you.
There's no difference. The reason you see most people doing it inside of the constructor is because doing state = {} directly on the class is new syntax that hasn't been widely adopted yet (it often still requires a Babel or similar transformation). See proposal-class-fields for more information on it. One thing to note is that if you need to access any props to initialize the state, you have to do that in the constructor.
Related
I have the following case:
I have a standard store with optional items in it.
I also have a tree of elements which rely on that store. I also select {account} in multiple components.
For business logic, I had to check at the very top if account is set. If it is not, I don't render the components which rely on it.
How can I tell TS that even though the value is optional in store I'm 100% sure it is NOT undefined?
Example code:
// store
interface Account {
id: number;
name: string;
}
export interface AppState {
account?: Account;
}
const initialState: AppState = {};
const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount(state: AppState, action: PayloadAction<Account | undefined>) {
state.account = action.payload;
}
}
});
// component
const GrandChild = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild /> : <>account not set</>;
};
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
// dispatch(setAccount({ id: 0, name: "john" }));
});
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Child />
</div>
);
}
Codesandbox:
https://codesandbox.io/s/required-reducer-ysts49?file=/src/App.tsx
I know I can do this:
const { account } = useSelector((state: RootState) => state, shallowEqual) as Account;
but this seems very hacky. Is there a better way?
Well you know that Grandchild wont be rendered if account is undefined. so why not pass account as a property to the Grandchild component. This way you could define the property as never being undefined. And since you only render Grandchild after you checked that account isn't undefined you should be able to pass account as the property to the component (and since you defined the property as not being undefined TS will not object to account.name in your Grandchild component.
I don't know redux however - I have never used it and don't know anything about it, so I don't know if this answer is compatible with that or if redux will cause some issues I couldn't forsee.
I've written a little bit of code of how this could look (but as I already said, I don't know how to use redux, so you'll probably have to take my idea and write it so everything works) - so my code example is probably more of a visualization of what I mean than a solution.
const GrandChild = (account: Account) => {
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild account={account} /> : <>account not set</>;
};
Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering
As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode
I am trying to create a autocomplete component. It's an input where user types the countru name and if letters match name of some country, the hints are displayed.
In my App Component i have method handleChange Within this method i change my state two times, which is bad idea.
How can I split it to change state in distinct methods ?
import React, { Component } from 'react';
import AutoComplete from './autoComplete.jsx';
import data from './data.json';
class App extends Component {
constructor(props) {
super(props);
this.state = {
inputValue: '',
resoults: []
}
}
handleChange() {
let inputValue = this.refs.input.value;
this.setState({
inputValue: inputValue
});
let regular = "^" + this.state.inputValue;
let reg = new RegExp(regular , "i");
let filtered = data.filter((i,index)=> {
return (reg.test(i.name)
);
});
console.log(filtered);
this.setState({resoults:filtered})
}
render() {
return (
<div>
<input onChange={this.handleChange.bind(this)} type="text" ref="input"/>
<h3>You typed: {this.state.inputValue}</h3>
<AutoComplete resoults={this.state.resoults} />
</div>
);
}
}
export default App;
import React, {Component} from 'react';
class AutoComplete extends Component {
render() {
return (
<div>
<h4>autocompleteComponent</h4>
{this.props.resoults.map((i)=> {
return (
<ul>
<li>{i.name}</li>
</ul>
);
})}
</div>
);
}
}
export default AutoComplete;
I found myself in this position many times, but I got to the conclusion that it's better to compute the autocomplete options (in your case) without having them in the state of your component.
As I have used them until now, the state and props of a component should represent minimal data needed to render that specific component. Since you have your input value in the state, having the autocomplete options there also seems redundant to me. So here is what I propose:
class App extends Component {
this.state = {
inputValue: '',
};
handleChange(e) {
const inputValue = e.target.value;
this.setState({
inputValue,
});
}
computeResults() {
const {inputValue} = this.state;
// your functionality for computing results here
}
render() {
const {inputValue} = this.state;
const results = this.computeResults();
return (
<div>
<input type="text" onChange={this.handleChange.bind(this)} value={inputValue} />
<h2>You typed: {inputValue}</h2>
<Autocomplete results={results} />
</div>
);
}
}
Notes
Since your results come synchronously, via the .json import, this seems the perfect solution to me. If you want to get them via fetch or anything else, then you'll have to figure out a slightly different approach, but keep in mind that the state of your component should not contain redundant data.
Stop using ref with string value! and use refs when there is absolutely no other way because a React component should not generally deal with DOM operations directly. If you really need to use refs, use ref callbacks.
Hope this helps!
Use another function and setState callBack:
handleChange() {
let inputValue = this.refs.input.value;
this.setState(
{
inputValue: inputValue
},
() => this.secondFunc()
);
}
secondFunc() {
let regular = '^' + this.state.inputValue;
let reg = new RegExp(regular, 'i');
let filtered = data.filter((i, index) => {
return reg.test(i.name);
});
console.log(filtered);
this.setState({ resoults: filtered });
}
I'm having a issue with a component I've built. One value (inclVal) must be larger than another (exclVal) if both are entered. I wanted to run the function that handles this with a setTimeout() so that it wouldn't update for a second after the props stop changing to ensure it wouldn't change the value the user is entering while she is entering it. To this end, I put in a clearTimeout() in an else block to prevent the function from executing if the props change so as to make it redundant. The problem is that clearTimeout() isn't working for some reason and the update function is running whenever the if block is entered, even though the else block is being entered within the timeout interval.
The component is a stateless functional component and is using redux for state management. I've read a bunch on how to make these things work, but nothing seems to be helping. Any help is appreciated!
Here is the code:
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import SelectPickerPulldown from '../../components/SelectPickerPulldown'
import TextInput from '../../components/TextInput'
import { odOptions } from '../../config/'
import { setODProperty } from '../../actions/odAnalyzerActions'
import { getConversion, getGreaterVal } from '../../utils/'
const InclusionExclusionOptions = ({ name,
analysisPoint,
paths,
exclVal,
exclUnit,
inclVal,
inclUnit,
getVal,
getUnit,
setSafeInclVal,
}) => {
const disabled = analysisPoint !== null || paths ? false : true
let inclEntryTimer = null
const exclCompare = getConversion(exclUnit)(exclVal)
const inclCompare = getConversion(inclUnit)(inclVal)
if (exclVal > 0 && inclVal > 0 && exclCompare > inclCompare) {
const safeInclVal = getGreaterVal(exclVal, exclUnit, inclUnit)
console.log('entering timeout');
inclEntryTimer = setTimeout( () => {
console.log('dispatching timeout action');
setSafeInclVal(safeInclVal)
}, 1000)
}
else {
console.log('clearing timeout');
clearTimeout(inclEntryTimer)
inclEntryTimer = null
}
return (
<div className="form-group" >
<h4>Inclusion/Exclusion Options</h4>
<ul className={name}>
<li className={`text-select-group ${disabled ? name + ' disabled' : name}`}>
<p>Exclusion Radius</p>
<div className="radius-setting">
<TextInput
type="number"
name="exclVal"
value={exclVal}
onChange={getVal}
/>
<SelectPickerPulldown
value={'exclUnit'}
options={odOptions.units}
selected={exclUnit}
getSelected={getUnit}
/>
</div>
</li>
<li className={`text-select-group ${disabled ? name + ' disabled' : name}`}>
<p>Inclusion Radius</p>
<div className="radius-setting">
<TextInput
type="number"
name="inclVal"
value={inclVal}
onChange={getVal}
/>
<SelectPickerPulldown
value={'inclUnit'}
options={odOptions.units}
selected={inclUnit}
getSelected={getUnit}
/>
</div>
</li>
</ul>
</div>
)
}
InclusionExclusionOptions.propTypes = {
name: PropTypes.string,
exclVal: PropTypes.number,
exclUnit: PropTypes.string,
inclVal: PropTypes.number,
inclUnit: PropTypes.string,
getVal: PropTypes.func,
getUnit: PropTypes.func,
}
const mapStateToProps = (state, ownProps) => {
const name = 'inclusion-exclusion-options'
const { analysisPoint,
paths,
exclVal,
exclUnit,
inclVal,
inclUnit } = state.odAnalyzerState
return {
name,
analysisPoint,
paths,
exclVal,
exclUnit,
inclVal,
inclUnit,
}
}
const mapDispatchToProps = dispatch => {
return {
getUnit: option => dispatch(setODProperty(option)),
getVal: (e, name) => dispatch(setODProperty({[name]: parseInt(e.target.value)})),
setSafeInclVal: safeInclVal => dispatch(setODProperty({inclVal: safeInclVal}))
}
}
export default connect(
mapStateToProps,
mapDispatchToProps)(InclusionExclusionOptions)
Here is the updated code with a class component using componentDidUpdate():
class InclusionExclusionOptions extends React.Component{
constructor(props){
super(props)
this.inclEntryTimer = null
}
componentDidUpdate(props){
const { exclVal,
exclUnit,
inclVal,
inclUnit,
} = this.props
const exclCompare = getConversion(exclUnit)(exclVal)
const inclCompare = getConversion(inclUnit)(inclVal)
if (!!exclVal && !!inclVal && exclCompare > inclCompare) {
const safeInclVal = getGreaterVal(exclVal, exclUnit, inclUnit)
console.log('entering timeout')
this.inclEntryTimer = setTimeout( () => {
console.log('dispatching timeout action');
this.props.setSafeInclVal(safeInclVal)
}, 3000)
}
else {
console.log('clearing timeout');
clearTimeout(this.inclEntryTimer)
this.inclEntryTimer = null
}
}
render() {
const { name,
analysisPoint,
paths,
exclVal,
exclUnit,
inclVal,
inclUnit,
getVal,
getUnit,
} = this.props
const disabled = analysisPoint !== null || paths ? false : true
return (
<div className="form-group" >
<h4>Inclusion/Exclusion Options</h4>
<ul className={name}>
<li className={`text-select-group ${disabled ? name + ' disabled' : name}`}>
<p>Exclusion Radius</p>
<div className="radius-setting">
<TextInput
type="number"
name="exclVal"
value={exclVal}
onChange={getVal}
/>
<SelectPickerPulldown
value={'exclUnit'}
options={odOptions.units}
selected={exclUnit}
getSelected={getUnit}
/>
</div>
</li>
<li className={`text-select-group ${disabled ? name + ' disabled' : name}`}>
<p>Inclusion Radius</p>
<div className="radius-setting">
<TextInput
type="number"
name="inclVal"
value={inclVal}
onChange={getVal}
/>
<SelectPickerPulldown
value={'inclUnit'}
options={odOptions.units}
selected={inclUnit}
getSelected={getUnit}
/>
</div>
</li>
</ul>
</div>
)
}
}
Per Brandon's suggestion, I was able to get the timeout cleared by simply clearing it before redeclaring it. I broke out a clear timeout function as
clearInclEntryTimer(){
clearTimeout(this.inclEntryTimer)
this.inclEntryTimer = null
}
then called it at the top of the if block and in the else block. That worked well. Thanks for the help!
The actual problem is that everytime your component renders, it generates a new instance of inclEntryTimer (and all other local variables) and so there's no way for a subsequent call to ever clear the timeout started in a previous call.
The conceptual problem is that your component is stateful, not stateless. Your requirements are such that the component needs to track time as state (specifically the timer). Change your stateless component to a traditional stateful component and you'll be able to store the timer id as a property of the class instance. You can then use componentDidUpdate(prevProps) life cycle event to clear the timeout if the conditions are met.
Update:
Based on what you've tried, the real problem is that you aren't clearing the old timers on every prop change. So think about what happens if the props change and you start a timer, the props change again and it is still higher so you start a 2nd timer and never clear the first, the props change again and you start a 3rd timer and so on. Finally the props change and you stop the last timer. But the first 5 timers are still running. So you should clear the existing timer everytime you start a new one.
But if you step back from the problem slightly...you don't need to implement this pattern yourself. What you are doing is something known as "debouncing". So use someone else's debounce algorithm.
Here's how to do it with lodash:
import debounce from 'lodash/debounce';
export default class Component from React.Component {
componentDidMount() {
this.correctValues(this.props);
}
componentDidUpdate() {
this.correctValues(this.props);
}
componentWillUnmount() {
// prevent the debounced method from running after we unmount
this._unmounted = true;
}
render() {
return <div>...</div>;
}
// we use debounce to essentially only run this function 3000 ms after
// it is called. If it gets called a 2nd time, stop the first timer
// and start a new one. and so on.
correctValues = debounce(props => {
// make sure we are still mounted
if (!this._unmounted) {
// need to correct the values!
if (props.a < props.b) {
props.setCorrectValue(props.a);
}
}
}, 3000);
}