react controlled input component with local and redux state, how to manage - reactjs

I have a controlled input component with some action on the value which is passed to local and redux state.
At the moment on the onchange event im calling a function which "clean up" the value and then it send it to the local state and also send it to the redux state, as you can see in the following code.
componentDidUpdate(prevProps: Readonly<Props>): void {
if (this.props.value !== prevProps.value) {
this.updateInputValue(this.props.value);
}
}
handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value || '';
if (value === '' || NUMBER_REGEX.test(value)) {
this.updateInputValue(value);
this.props.onChange(e);
}
};
updateInputValue = (inputValue: string): void => this.setState({ inputValue });
onChangeInputField = (e: any): void => {
const { alphanumeric, uppercase } = this.props;
if (trim) e.target.value = e.target.value.replace(/\s/g, '');
if (alphanumeric && ALPHANUMERIC_REGEXP.test(e.target.value)) return;
if (uppercase) e.target.value = e.target.value.toUpperCase();
this.updateInputValue(e.target.value);
this.props.onChange(e);
};
My concern here is that:
onChangeInputField triggers:
updateInputValue which updates state value.
props.onChange which updates redux store value
because something was updated, now we're calling componentDidUpdate which:
Checks if received props are matching because we updated value in store with props.onChange, we're receiving new prop values
because of that updateInputValue is once again triggered - which updates state again.
The problems i see here is that first of all we have multiple sources of truth
Secondly i'm rerendering element multiple times.
what would be the correct flow?

Related

usestate in react for setting state

Hi in my application we are using initialstate where the application sample data will be defined and using the context for state management, below is my sample initial state:
-
creditCard: {
isSaved: false,
lastFourDigits: "1235",
loading: false,
cardholder: "",
cardnumber: "",
cardmonth: "",
cardyear: "",
cardcvv: "",
},
etc:{}....
and in my component i am using usestate for setting the data as below :
const { state,actionsCollection } = useContext(StateContext);
const [cardholder, setcardholder] = useState("");
const [cardnumber, setcardnumber] = useState("");
const [cardmonth, setcardmonth] = useState("");
const [cardyear, setcardyear] = useState("");
const [cardcvv, setcardcvv] = useState("");
and in onchange i am setting state as below:
<TextField
name="cardmonth"
label="MM"
error={errors.cardmonth}
value={cardmonth}
onChange={onChange}
onBlur={validateInput}
helperText={errors.cardmonth && "Invalid month"}
className={classes.expiryDateInputs}
/>
onchange=()=>{
let cardMonth = /^0[1-9]|1[0-2]/.test(e.target.value);
if (cardMonth === true) {
setcardmonth(e.target.value.replace(/\D/g, "").slice(0, 2));
setErrors({ ...errors, cardmonth: false });
} else {
setErrors({ ...errors, cardmonth: true });
}
if (state.creditCard.cardyear !== "") {
validateExpiryDate();
}
}
passing states to non related components using below code:
const validateForm = () => {
return actionsCollection.booking.validateForm(
errors,
setErrors,
cardholder,
setcardholder,
cardnumber,
setcardnumber,
cardType,
setCardType,
cardmonth,
setcardmonth,
cardyear,
setcardyear,
cardcvv,
setcardcvv,
isCurrentCaseIncluded,
cardYearValue
);
};
and in actions i am using this code:
const validateForm = (
errors,
setErrors,
cardholder,
setcardholder,
cardnumber,
setcardnumber,
cardType,
setCardType,
cardmonth,
setcardmonth,
cardyear,
setcardyear,
cardcvv,
setcardcvv,
isCurrentCaseIncluded,
cardYearValue
) => {
some validation logic.....
}
is this the correct way what i am doing, can anyone please tell me what i am doing in onchange and in html code is correct or not..
If you need to validate your input inside onChange this code is perfectly fine.
But ,It's not a good idea to validate input on the onChange event . Put your validation logic inside onBlur or onSubmit events and in onChange event , simply set state to event value.
also, I would recommend better naming convention.
onChange={(e)=>handleOnChange(e)}
///////////
handleOnChange=(event)=>{
setcardmonth(event.target.value);
}
// move your validation logic to onBlur or onSubmit
Seems like you have defined your initialState up on the context/global level, but at the same time redefined it in one of your local component with a bunch more individual state:
const { state } = useContext(StateContext);
const [cardholder, setcardholder] = useState("");
const [cardnumber, setcardnumber] = useState("");
const [cardmonth, setcardmonth] = useState("");
const [cardyear, setcardyear] = useState("");
const [cardcvv, setcardcvv] = useState("");
That effectly makes your onChange function only update the state you redefined in your component, and it does not affect your context state, and won't update UI if the UI is dependent on context state.:
onchange=()=>{
let cardMonth = /^0[1-9]|1[0-2]/.test(e.target.value);
if (cardMonth === true) {
setcardmonth(e.target.value.replace(/\D/g, "").slice(0, 2));
setErrors({ ...errors, cardmonth: false });
} else {
setErrors({ ...errors, cardmonth: true });
}
if (state.creditCard.cardyear !== "") {
validateExpiryDate();
}
Meaning you are doing some extra unnecessary work by simplying redefinning things/states.
You can do one of the three ways:
Go straight for local state without global state, and if some child components needs them, you can just pass props and drill them down;
Or combine global and local state: for those states that need to be shared between different non-related components, you define it in global level, ie. your initialstate, for those that doesn't, put them in a local state;
Or go straight for global state.
On the straight global state approach, you can combine useReducer with context, and that makes updating state more managable(considering your initialstate is relatively complex), otherwise, you can pass setState function:
const { state, setState } = useContext(StateContext); // <- pass down setState from context as well
// no need for local states
// this is how to use state value from context:
<TextField
name="cardmonth"
label="MM"
error={errors.cardmonth}
value={state.cardmonth} // <- access state value using dot notation
onChange={onChange}
onBlur={validateInput}
helperText={errors.cardmonth && "Invalid month"}
className={classes.expiryDateInputs}
/>
// this is how to update value from context:
onchange=()=>{
let cardMonth = /^0[1-9]|1[0-2]/.test(e.target.value);
if (cardMonth === true) {
setState(); // <- your update logic
} else {
setState(); // <- your update logic
}
if (state.creditCard.cardyear !== "") {
validateExpiryDate();
}

Component did update behaving abnormally

I have implemented a block where i'm using componentDidUpdate to call a function if 2 of my condition meets the certain criteria. When both the condition satisfies it is calling the function which is then going to an infinite loop and executing the function for infinite times.
I have 2 drop-downs which are there to select the values. If both of them are having values, then call the function for this I'm using componentDidUpdate to keep an eye on changes on both the state variables.
When drop-down value change it will set the state to state variable which i'm using in the condition.
Below is my code:
handleRegionChange(idx: any, event: any) {
const region= [""];
region[idx] = event.label;
this.setState({ region});
}
handleProductChange(idx: any, event: any) {
const productId= [""];
productId[idx] = event.key;
this.setState({ productId});
}
componentDidUpdate() {
if (
this.state.regionId?.[0] && this.state.productId?.[0]) {
this.props.fetchValues( // Redux action function which accepts 2 parameters
this.state.regionId?.[0],
this.state.productId?.[0]
);
}}
Please help me in highlighting the issue or through some light on how to tackle or use component did update in this kind of situation.
Thanks.
It's causing an infinite loop mostly likely because fetchValues will update your component props from it's parent, this triggers another update, which will run componentDidUpdate again.
One easy way to fix this is to prevent further update, if the id has not been changed for any dropdown values.
componentDidUpdate(prevProps, prevState) {
const hasRegionIdChanged = this.state.regionId !== prevState.regionId;
const hasProductIdChanged = this.state.productId !== prevState.productId;
if (hasRegionIdChanged || hasProductIdChanged ) {
this.props.fetchValues(
this.state.regionId?.[0],
this.state.productId?.[0]
);
}
}
https://reactjs.org/docs/react-component.html#componentdidupdate
Further reading, see how React introduced the hook pattern to let you think of these things beforehand, requiring a dependencies list:
https://reactjs.org/docs/hooks-reference.html#useeffect
2nd attempt:
// Not sure if this is a multi select dropdown,
// It should be either [] or a plain value
// Multi select dropdown
handleRegionChange(idx: any, event: any) {
// You should not need to know which index is was, all you need is the region label ... (Updated for remove of duplication)
// If you have access to the spread operator, then JSON.stringify is not required below
const region = [...new Set([...this.state.region, event.label])];
this.setState({ region});
}
// Single select dropdown
handleProductChange(idx: any, event: any) {
this.setState({ productId: event.key});
}
componentDidUpdate(prevProps, prevState) {
// If it's an array/object, you need to compare them deeply
const hasRegionIdChanged = JSON.stringify(this.state.regionId) !== JSON.stringify(prevState.regionId);
const hasProductIdChanged = this.state.productId !== prevState.productId;
if (hasRegionIdChanged || hasProductIdChanged ) {
if (this.state.regionId?.length && this.state.productId) {
this.props.fetchValues(
this.state.regionId,
this.state.productId
);
}
}
}

React input field does now work when typing too fast

I have this simple component that checks if username is valid. It does so by querying firebase when the input value changes. There is one problem with it. When I am typing too fast into the input field, the value in it just doesn't have enough time to change, so it just misses some characters. Here is the code:
For state management I am using Recoil.JS.
Component code:
export const UsernameInput = (props: {
topLabel: string;
bottomLabel?: string;
placeholder?: string;
className?: string;
valueIn: any;
valueOut: any;
valid: any;
validIn: boolean;
}) => {
const usernameRef = db.collection("usernames");
const query = usernameRef.where("username", "==", props.valueIn);
useEffect(() => {
query
.get()
.then((querySnapshot) => {
if (querySnapshot.size >= 1) {
props.valid(false);
} else {
props.valid(true);
}
})
}, [props.valueIn]);
function handleChange(event: any) {
props.valueOut(event.target.value);
}
return (
<InputSkeleton
topLabel={props.topLabel}
bottomLabel={props.bottomLabel}
className={props.className}
>
<div className="input-username">
<input type="text" onChange={handleChange} value={props.valueIn} />
<span className="text">
<span className={props.validIn ? "available" : "taken"}></span>
{props.validIn ? "Available" : "Taken"}
</span>
</div>
</InputSkeleton>
);
};
<UsernameInput
className="stretch"
topLabel="Username"
valueIn={formD.username}
valueOut={(value: string) => {
setFormD({ ...formD, username: value });
}}
valid={(value: boolean) => {
setFormD({ ...formD, usernameValid: value });
}}
validIn={formD.usernameValid}
bottomLabel="This will be your unique handle on xyz.com"
/>
Princewill's idea is the right one, but the implementation needs a little tweaking. Specifically, you need the timer handle to be preserved across multiple invocations of debounce, and the argument to debounce needs to be an actual function. Using a plain function doesn't do it, because each invocation results in a different local timeout handle, and the old handle never gets cancelled or updated.
I recommend adapting or using the useDebounce hook from useHooks. This uses useEffect to exploit React's effect unmounting to clear any previously-set timeouts, and is pretty clear overall.
const { valueIn, valueOut } = props;
const [username, setUsername] = useState<string>(valueIn);
// On each event, update `username`
const handleChange = useCallback(
(event: any) => setUsername(event.target.value),
[setUsername]
);
// Collect changes to username and change debouncedUsername to the latest
// value after a change has not been made for 500ms.
const debouncedUsername = useDebounce(username, 500);
// Each time debouncedUsername changes, run the desired callback
useEffect(() => {
if (debouncedUsername !== valueIn) {
valueOut(debouncedUsername);
}
}, [valueIn, valueOut, debouncedUsername]);
The idea here is:
You keep a realtime-updated copy of the field state via useState
You keep a delay-updated copy of the field state via useDebounce
When the delay-updated copy is finally changed, the useEffect fires your valueOut callback. As constructed, this would fire after username has changed, but has not changed again for 500ms.
Additionally, you would want to set your field's value to username, rather than valueIn, so that the field is updated in realtime, rather than on the delay.
Create a simple debounce function that takes a function and time in secs as parameters:
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
Then use it in your event handler handleChange function:
function handleChange(event: any) {
event.preventDefault();
// This means that you want to update the value after 500 milliseconds, i.e when you're sure that the user has stopped typing. You can extend this time to whatever figure you want
debounce(props.valueOut(event.target.value), 500);
}
Put this variable outside UsernameInput function
const WAIT_INTERVAL = 1000;
Edit your handleChange to this
componentWillMount() {
this.timer = null;
}
function handleChange(event: any) {
clearTimeout(this.timer);
this.timer = setTimeout(props.valueOut(event.target.value), WAIT_INTERVAL);
}

react setState not updating state in one of my functions

I'm working an a react app with a few forms and I am trying to implement an edit form for input items. The function first opens the list item in a pre-populated form.
The editItem function currently looks like this:
editItem(event) {
event.preventDefault();
const target = event.target.parentNode.parentNode;
const { key } = target.dataset;
const { className } = target;
const currState = { ...this.state[className] };
const currItem = currState.list[key];
for (let i in currItem) {
if (i !== "list" && i !== "hidden") {
currState[i] = currItem[i]
}
}
this.setState({ [className]: currState });
this.hideUnhide({target: {name: className}});
}
I have confirmed with console logs that currState is correctly set with the values that I am looking for, and that I am not having an async issue. I am using this same format to set state in other functions in my app and all of the others are working properly. If I directly mutate state in the same place, I get the behavior I'm looking for (form fields populate), but nothing happens when I use setState.
Link to my github repo: here. The function in question is in App.js.
As Brian Thompson points out in his comment, it turns out that the hideUnhide function call directly after my setState uses setState as well and writes over the first setState call with the previous state:
hideUnhide(event) {
const { name } = event.target;
const currState = { ...this.state[name] };
if (currState.hidden === true) {
currState.hidden = false;
}
this.setState({ [name]: currState });
}
The way to prevent that was to use hideUnhide as a callback to the setState in editItem:
this.setState({ [className]: currState }, () =>
this.hideUnhide({ target: { name: className } })
);
and now everything functions as intended.

Typescript class as React state, rerender

I have the following function component:
const Player = () => {
const [spotifyPlayer, setSpotifyPlayer] = React.useState<SpotifyPlayer | null>(null);
return (
<SongControls togglePlay={togglePlay} isPlaying={spotifyPlayer.isPlaying} />
);
};
the SongControls component is responsible for showing the correct control button (start/pause):
const SongControls: React.FC<Props> = ({ isPlaying }: Props) => {
return (
{isPlaying ? (
<Pause style={iconBig} onClick={togglePlay} />
) : (
<PlayArrow style={iconBig} onClick={togglePlay} />
)}
);
};
the spotifyPlayer state in the Player component is a class that has a field isPlaying of type boolean. It also has a function defined togglePlay() which toggles isPlaying.
The problem is that when togglePlay() is called React doesn't rerender the components. I understand why (since the instance of SpotifyPlayer is not changing, thats why you shouldn't update react state directly).
I can't call setSpotifyPlayer() to update the state because of 2 reasons:
the instance of SpotifyPlayer is responsible for toggling isPlaying
when I change the state use js deconstructing I lose the prototype of the spotifyPlayer state and the function won't be available anymore.
I am not able to figure this out.
Your understanding of the problem is correct. You cannot rely on changes to the internal state of the SpotifyPlayer object to trigger a re-render. I recommend using a local state to store the value of isPlaying and update the SpotifyPlayer based on changes to that value.
If the player object has some sort of onChange listener, then I would use the player to set the local state rather than the other way around.
I'm not sure what package the player comes from, so I am assuming this interface:
class SpotifyPlayer {
isPlaying: boolean = false;
play(): void {
this.isPlaying = true;
}
pause(): void {
this.isPlaying = false;
}
}
We can use useRef to store a consistent reference to a player object.
const spotifyPlayer = React.useRef<SpotifyPlayer>(new SpotifyPlayer()).current;
We will store the value of isPlaying in local state. We can get the initial value from the player.
const [isPlaying, setIsPlaying] = React.useState(spotifyPlayer.isPlaying);
In order to keep isPlaying in sync with the player, we could use useEffect.
const togglePlay = () => setIsPlaying(!isPlaying);
React.useEffect(() => {
if ( isPlaying && ! spotifyPlayer.isPlaying ) {
spotifyPlayer.play();
} else if ( ! isPlaying && spotifyPlayer.isPlaying ) {
spotifyPlayer.pause();
}
}, [spotifyPlayer, isPlaying]);
But if togglePlay is the only function which will change the value, then we could handle the changes in that function.
const togglePlay = () => {
if (isPlaying ) {
spotifyPlayer.pause();
setIsPlaying(false);
} else {
spotifyPlayer.play();
setIsPlaying(true);
}
}

Resources