I have a component that uses useState() to handle the state of its floating label, like this:
const FloatingLabelInput = props => {
const {
value = ''
} = props
const [floatingLabel, toggleFloatingLabel] = useState(value !== '')
I have a series of those components and you'd expect initialFloatingLabel and floatingLabel to always be the same, but they're not for some of them! I can see by logging the values:
const initialFloatingLabel = value !== ''
console.log(initialFloatingLabel) // false
const [floatingLabel, toggleFloatingLabel] = useState(initialFloatingLabel)
console.log(floatingLabel) // true???
And it's a consistent result. How is that possible?
How come value can be different from initialValue in the following example? Is it a sort of race condition?
const [value, setValue] = useState(initialValue)
More details here
UPDATE
This (as suggested) fixes the problem:
useEffect(() => setFloatingLabel(initialFloatingLabel), [initialFloatingLabel])
...but it creates another one: if I focus on a field, type something and then delete it until the value is an empty string, it will "unfloat" the label, like this (the label should be floating):
I didn't intend to update the floatingLabel state according to the input value at all times; the value of initialFloatingLabel was only meant to dictate the initial value of the toggle, and I'd toggle it on handleBlur and handleChange events, like this:
const handleFocus = e => {
toggleFloatingLabel(true)
}
const handleBlur = e => {
if (value === '') {
toggleFloatingLabel(false)
}
}
Is this approach wrong?
UPDATE
I keep finding new solutions to this but there's always a persisting problem and I'd say it's an issue with Formik - it seems to initially render all my input component from its render props function before the values are entirely computed from Formik's initialValues.
For example:
I added another local state which I update on the handleFocus and handleBlur:
const [isFocused, setFocused] = useState(false)
so I can then do this to prevent unfloating the label when the input is empty but focused:
useEffect(() => {
const shouldFloat = value !== '' && !isFocused
setFloatLabel(shouldFloat)
}, [value])
I'd still do this to prevent pre-populated fields from having an animation on the label from non-floating to floating (I'm using react-spring for that):
const [floatLabel, setFloatLabel] = useState(value !== '')
But I'd still get an animation on the label (from "floating" to "non-floating") on those specific fields I pointed out in the beginning of this thread, which aren't pre-populated.
Following the suggestion from the comments, I ditched the floatingLabel local state entirely and just kept the isFocused local state. That's great, I don't really need that, and I can only have this for the label animation:
const animatedProps = useSpring({
transform: isFocused || value !== '' ? 'translate3d(0,-13px,0) scale(0.66)' : 'translate3d(0,0px,0) scale(1)',
config: {
tension: 350,
},
})
The code looks cleaner now but I still have the an animation on the label when there shouldn't be (for those same specific values I mentioned at the start), because value !== '' equals to true for some obscure reason at a first render and then to false again.
Am I doing something wrong with Formik when setting the initial values for the fields?
You have the use useEffect to update your state when initialFloatingLabel change.
const initialFloatingLabel = value !== ''
const [floatingLabel, setFloatingLabel] = useState(initialFloatingLabel)
// calling the callback when initialFloatingLabel change
useEffect(() => setFloatingLabel(initialFloatingLabel), [initialFloatingLabel])
...
Your problem look like prop drilling issue. Perhaps you should store floatingLabel in a context.
// floatingLabelContext.js
import { createContext } from 'react'
export default createContext({})
// top three component
...
import { Provider as FloatingLabelProvider } from '../foo/bar/floatingLabelContext'
const Container = () => {
const [floatingLabel, setFloatingLabel] = useState(false)
return (
<FloatingLabelProvider value={{ setFloatingLabel, floatingLabel }}>
<SomeChild />
</FloatingLabel>
)
}
// FloatingLabelInput.js
import FloatingLabelContext from '../foo/bar/floatingLabelContext'
const FloatingLabelInput = () => {
const { setFloatingLabel, floatingLabel } = useContext(FloatingLabelContext)
...
}
This way you just have to use the context to change or read the floatingLabel value where you want in your components three.
Related
I am new to React. I have the following setup to initialise my component's state based on a condition, using a ternary operation.
const defaultValue = ['bar']
const myComponent = ({data}) => {
const foo = (data) => {
// ...
}
const [values, setValues] = Object.keys(data).length > 0 ? useState(foo(data)): useState(defaultValue);
return (<Comp state={values} />);
}
export default myComponent
The idea is that the initial state will be dependent on the variable 'data' if it is not empty, otherwise it will be assigned a default value. It is now assumed that 'data' is not empty.
I have it that foo() is being called when I expect it, and the argument, data, is also as I expect. If I log the array that foo returns, it is exactly as I want. Unfortunately, when I log 'state' from inside the Comp component, it is the defaultValue.
If I add a console.log("renders") towards the top of myComponent, I get that printout twice, which I suspect is part of the problem?
If this setup is unconventional please let me know and I'll refactor, but if there is a simple work around that somebody is aware of, please let me know!
It's difficult to give better advice without seeing a bit more code. However, you can use a functional value to initialize the state:
const defaultValue = ['bar'];
const MyComponent = ({data}) => {
const foo = (data) => {
// ...
};
const [values, setValues] = useState(() => Object.keys(data).length > 0 ? foo(data) : defaultValue);
return (<Comp state={values} />);
}
Or just modify the foo closure to fulfill that purpose:
const defaultValue = ['bar'];
const MyComponent = ({data}) => {
const foo = () => {
if (Object.keys(data).length === 0) return defaultValue;
// ...
};
const [values, setValues] = useState(foo);
return (<Comp state={values} />);
}
Using this form has the additional benefit of preventing foo from being invoked needlessly on every render of your component.
The code has a problem because useState is used inside a ternary condition
Object.keys(data).length > 0 ? useState(foo(data)): useState(defaultValue);
We shouldn't useState hook inside any condition check because it'll affect the component rendering as seen in the doc.
To solve the problem
const initialValue = Object.keys(data).length > 0 ? foo(data) : defaultValue;
const [values, setValues] = useState(initialValue);
Ref:
https://beta.reactjs.org/learn/state-a-components-memory#meet-your-first-hook
Edit: Small changes for readability.
I'm new to react and I may be in at the deep end here but I'll go ahead anyway..
I have a Login component in which I want to give the users feedback when the input elements lose focus and/or when the user clicks submit.
I am aware that I achieve a similar bahavior with useState but for the sake of education I'm trying with useRef.
I'm getting a TypeError for undefined reading of inputRef in LoginForm.js. So inputRef is not assigned a value when validateInput is called. Can anyone help me make sense of why that is and whether there is a solution to it?
LoginForm.js:
import useInput from '../../hooks/use-input';
import Input from '../../UI/Input/Input';
const LoginForm = () => {
const { inputRef, isValid } = useInput(value =>
value.includes('#')
);
return <Input ref={inputRef} />;
};
use-input.js (custom hook):
const useInput = validateInput => {
const inputRef = useRef();
const isValid = validateInput(inputRef.current.value);
return {
inputRef,
isValid,
};
};
Input.js (custom element component):
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props.input}></input>;
});
One issue that I'm seeing is that in the Input component, you're using props.input, why?
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props}></input>;
});
You want exactly the props that you're sending to be assigned to the component.
Next up, you're doing value.includes('#'), but are you sure that value is not undefined?
const { inputRef, isValid } = useInput(value =>
value && value.includes('#')
);
This would eliminate the possibility of that error.
Solving the issue with the inputRef is undefined is not hard to fix.
Afterward, you're going to face another issue. The fact that you're using useRef (uncontrolled) will not cause a rerender, such that, if you update the input content, the isValid won't update its value.
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. (React Docs)
This is a personal note, but I find uncontrolled components in general hard to maintain/scale/..., and also refs are not usually meant to do this kind of stuff. (yes, yes you have react-form-hook which provides a way of creating forms with uncontrolled components, and yes, it's performant).
In the meantime, while I'm looking into this a little more, I can provide you a solution using useState.
const useInput = (validationRule, initialValue='') => {
const [value, setValue] = useState(initialValue)
const onChange = (e) => setValue(e.target.value)
const isValid = validationRule && validationRule(value)
return {
inputProps: {
value,
onChange
},
isValid
}
}
So, right here we're having a function that has 2 parameters, validationRule and initialValue(which is optional and will default to text if nothing is provided).
We're doing the basic value / onChange stuff, and then we're returning those 2 as inputProps. Besides, we're just calling the validationRule (beforehand, we check that it exists and it's sent as parameter).
How to use:
export default function SomeForm() {
const { inputProps, isValid } = useInput((value) => value.includes('#'));
return <Input {...inputProps}/>;
}
The following part is something that I strongly discourage.
This is bad but currently, the only way of seeing it implemented with refs is using an useReducer that would force an update onChange.
Eg:
const useInput = (validationRule) => {
const [, forceUpdate] = useReducer((p) => !p, true);
const inputRef = useRef();
const onChange = () => forceUpdate();
const isValid = validationRule && validationRule(inputRef.current?.value);
return {
inputRef,
isValid,
onChange
};
};
Then, used as:
export default function SomeForm() {
const { inputRef, onChange, isValid } = useInput((value) =>
value && value.includes("#")
);
console.log(isValid);
return <Input ref={inputRef} onChange={onChange} />;
}
I'm attempting to build a form in React that has some auto-filling features. I'm trying to create the ability to 'lock' the value of LossOver50k to either 'Yes' or 'No' based on the ReplacementCost value to prevent input errors. The code I have right now is
import React, { useState } from "react";
import ReplacementCost from "./ReplacementCost";
import LossOver50K from "./LossOver50K";
const LossAmount = () => {
const [replacementCost, setReplacementCost] = useState("");
const [highValue, setHighValue] = useState("No");
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (replacementCost >= 50000) {
setHighValue("Yes");
}
};
// const changeHighValue = (newHighValue) => {
// setHighValue(newHighValue);
// };
return (
<div>
<ReplacementCost value={replacementCost} onChange={handleChange} />
<LossOver50K value={highValue} />
</div>
);
};
export default LossAmount;
My commented piece was working as an onChange handler on the LossOver50k component but I'm trying to manipulate the value of LossOver50k if the value of Replacement Cost is equal to or greater than 50,000. I have each of these components successfully updating their state when I input the values directly by passing the props down to the child components. Any ideas?
TIA!
Your condition should be with the most updated value and not with a staled one (as setState is async):
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (newReplacementCost >= 50000) {
setHighValue("Yes");
}
};
Considering the following declaration:
const [stateObject, setObjectState] = useState({
firstKey: '',
secondKey: '',
});
Are the following snippets both corrects ?
A)
setObjectState((prevState) => ({
...prevState,
secondKey: 'value',
}));
B)
setObjectState({
...stateObject,
secondKey: 'value',
}));
I am sure that A) is correct, but is it necessary ? B) seems ok, but as setObjectState is an asynchronous function, stateObject might not have the most recent value.
One useful thing about case of A that I have found is that you can use this method to update state from child components while only passing down a single prop for setObjectState. For example, say you have parent component with state you would like to update from the child component.
Parent Component:
import React, {useState} from 'react';
import ChildComponent from './ChildComponent';
export const ParentComponent = () => {
const [parentState, setParentState] = useState({
otherValue: null,
pressed: false,
});
return (
<ChildComponent setParentState={setParentState} />
)
}
Child Component:
import React from 'react';
export const ChildComponent = (props) => {
const callback = () => {
props.setParentState((prevState) => ({
...prevState
pressed: true,
}))
}
return (
<button onClick={callback}>test button<button>
)
}
When the button is pressed, you should expect to see that the state has been updated while also keeping its initial values. As for the difference between the two, there isn't much as they both accomplish the same thing.
A will always give you the updated value. B could be correct but might not. Let me give an example:
const Example = props => {
const [counter, setCounter] = useState(0);
useEffect(() => {
// 0 + 1
// In this first case the passed value would be the same as using the callback.
// This is because in this cycle nothing has updated counter before this point.
setCounter(counter + 1);
// 1 + 1
// Thanks to the callback we can get the current value
// which after the previous iexample is 1.
setCounter(latest_value => latest_value + 1);
// 0 + 1
// In this case the value will be undesired as it is using the initial
// counter value which was 0.
setCounter(counter + 1);
}, []);
return null;
};
When the new value depends on the updated one use the callback, otherwise you can simply pass the new value.
const Example = props => {
const [hero, setHero] = useState('Spiderman');
useEffect(() => {
// Fine to set the value directly as
// the new value does not depend on the previous one.
setHero('Batman');
// Using the callback here is not necessary.
setHero(previous_hero => 'Superman');
}, []);
return null;
};
Also in the example you are giving it would probably be better to use two different states:
const [firstKey, setFirstKey] = useState("");
const [secondKey, setSecondKey] = useState("");
I created useBanner hooks
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState(array.slice(0, yardage));
const [bannListIndex, setBannIndex] = useState(1);
return {
....
};
};
Am I doing the right thing and the props throw in useState.
It’s permissible to use useBanner.
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
when props will change here.
Will change the state in useBanner.
or is it considered anti-patterns I have to write all this in useMemo
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState([]);
const [bannListIndex, setBannIndex] = useState(1);
useMemo(() => {
setBannArr(array.slice(0, yardage));
setBannIndex(1);
}, [array, yardage]);
return {
....
};
};
Yes, custom hooks are possible in React. Here is separate document discussing custom hooks.
But exactly you sample may require additional code depending on what is your final goal.
If you want initialize state only once, when component Banner is first created, you can just do as in your first sample
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
This will work perfectly. But if props array and yardage will change, this will not be reflected in component. So props will be used only once as initial values and then will not be used in useBanner even if changed (And it doesn't matter whether you'll use useBanner or useState directly). This answer highlight this.
If you want to update inital values on each props change, you can go with useEffect like below
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr, setBannArr } = useBanner(array, yardage);
useEffect (() => {
// setBannArr should also be returned from useBanner. Or bannArr should be changed with any other suitable function returned from useBanner.
setBannArr(array.slice(0, yardage));
}, [array, yardage, setBannArr])
return (
...
);
};
In this case Banner component can control state itself and when parent component change props, state in Banner component will be reset to new props.
Here is small sample to showcase second option.