I am trying to get a form to work, which acts both as a creation- and an update-form for an item. I keep track of an edit state to decide what text should be rendered in the form and what API call to make on submitting it. So far so good, that all works well. In order to keep track of the input values, I use a hook useInputState to update the corresponding state value:
useInputState.js
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (e) => {
if (e.target) {
setValue(e.target.value);
} else { // to be able to set state manually
setValue(e);
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
const [newTitle, setNewTitle, resetNewTitle] = useInputState();
form.js
export default function Form({
newTitle,
setNewTitle,
edit, // true or false
}) {
return (
<div>
<h2>{edit ? "Edit Item" : "Add new item"}</h2>
<div>
<label>Title</label>
<input
type="text"
placeholder="What is your title?"
value={newTitle}
onChange={setNewTitle}
/>
</div>
)
}
Now when the user adds a new item, obviously the form is empty to begin with. However, when the user wants to edit an item, I try to prepopulate the form with the information of the given object. This is where I run into issues: The object model has some optional fields. When the user leaves these blank when creating the object, editing it breaks the application with an error of can't read property target of null.
I use the following code to prepoulate the form fields when edit is toggled on:
useEffect(() => {
if (edit && selectedItem) {
setNewTitle(selectedItem.title);
}
}, [edit]);
I see why it's running into issues and I tried a lot of things like changing the setNewTitle argument to setNewTitle(selectedItem["title"] !== undefined ? selectedItem.title : ""); and similar approaches but nothing has worked so far.
What can I do to solve this issue?
It seems that you are only comparing selectedItem.title !== undefined which can result in a specified error if your selectedItem.title is equal to null;
There are multiple ways to solve the issue but you can do the following:
const handleChange = (e) => {
if (!!e && e.target) {
setValue(e.target.value);
} else { // to be able to set state manually
setValue(e);
}
};
The change is in the line: if (!!e && e.target)
In your useInputState hook, you assume that handleChange function is a React.ChangeEvent meaning it is passed with e : Event from which you read e.target.value. This breaks when you try to set the value directly here in this line, in useEffect:
setNewTitle(selectedItem.title);
I would suggest a change like this to your useInputState hook,
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (value) => {
if (value) {
setValue(value);
} else {
setValue("");
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
then i your form do this,
export default function Form({
newTitle,
setNewTitle,
edit, // true or false
}) {
return (
<div>
<h2>{edit ? "Edit Item" : "Add new item"}</h2>
<div>
<label>Title</label>
<input
type="text"
placeholder="What is your title?"
value={newTitle}
onChange={(e)=>setNewTitle(e.target.value)}
/>
</div>
)
}
It should work as you expect it to work with this change.
Your error is can't read property target of null.
So the e arg is falsy sometimes.
Try
setNewTitle(selectedItem.title ? selectedItem.title : '')
and in the custom hook:
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (e) => {
if(typeof e === 'string') {
setValue(e);
} else if(e && e.target) {
setValue(e.target.value);
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
Related
Pretty new to React Hooks and I ran into this issue. I have a functional component that takes an input and sends it to the parent component when I hit the enter key (keycode = 13). The component looks something like this.
const SearchTermBar = (props) => {
const {resetStateSearchTerm, handlePowerToggleParent} = props;
const [inputTerm, handleInputChange] = useState('');
const inputRef = useRef();
useEffect(() => {
const keyPressEvent = (e) => {
if (e.keyCode === 13) {
resetStateSearchTerm(inputTerm);
handleInputChange('');
handlePowerToggleParent('search');
}
};
inputRef.current.addEventListener('keydown', keyPressEvent);
let parentInputRef = inputRef;
return () => {
console.log('remove event listener');
parentInputRef.current.removeEventListener('keydown', keyPressEvent);
}
}, [inputTerm, resetStateSearchTerm, handlePowerToggleParent]);
return (
<div className='SearchTermBar'>
<input
type='text'
placeholder='Enter search term here (Press return to confirm)'
className='SearchTermBar__Input'
value={inputTerm}
onChange={(e) => handleInputChange(e.target.value)}
ref={inputRef}
/>
</div>
);
The problem is that the event is registered and unregistered every time the inputTerm or props value changes. But I am not able to figure out the correct way to handle the event registration/removal (which should happen once ideally) I understand it is because of the dependency on the inputTerm but I would like to know a better solution to this problem.
You already has the input ref, you don't really need a state:
const NOP = () => {};
const DEFAULT_INPUT = "";
function SearchTermBar(props) {
const { resetStateSearchTerm = NOP, handlePowerToggleParent = NOP } = props;
const inputRef = useRef();
useEffect(() => {
const keyPressEvent = (e) => {
if (e.keyCode === 13) {
resetStateSearchTerm(inputRef.current.value);
inputRef.current.value = DEFAULT_INPUT;
handlePowerToggleParent("search");
}
};
inputRef.current.addEventListener("keydown", keyPressEvent);
let parentInputRef = inputRef;
return () => {
console.log("remove event listener");
parentInputRef.current.removeEventListener("keydown", keyPressEvent);
};
}, [resetStateSearchTerm, handlePowerToggleParent]);
return (
<input
type="text"
placeholder="Enter search term here (Press return to confirm)"
style={{ width: "50%" }}
ref={inputRef}
/>
);
}
Either way, if you want to keep the state, its value should be duplicated into a ref to fix the closure in the useEffect. It can be done by adding another useEffect which will update the mentioned ref.
my goal is to programmatically manage focus and blur and change style of select container correspondingly when user interacts with my form. Here is what I have done
const [inputValue, setInputValue] = useState("");
const handleFocus = (e) => {
// console.log(e.target.id);
setIsBlurred({ ...isBlurred, [e.target.id]: false });
};
const handleBlur = (e) => {
// handle blur
e.target.value.length === 0 &&
setIsBlurred({ ...isBlurred, [e.target.id]: true });
};
const handleChange = (e) => {
// console.log(e);
handleFormData({ ...formData, [e.id]: e.value });
setInputValue(e.value);
};
const style = {
control: (base, inputValue) => ({
...base,
borderColor: helpText && inputValue.length === 0 ? "red" : "e4e4e4",
}),
};
// rest of code //
<Select
options={options}
onChange={e => handleChange(e)}
inputValue={inputValue}
handleBlur={(e) => handleBlur(e)}
handleFocus={(e) => handleFocus(e)}
styles={style}
inputId="sensor_type"
helpText={/* a boolean value based on whether field is blurred or not*/}
/>
So there are two difficulties
When i'm giving the inputValue prop to the react select, after selecting the first element, I'm no longer able to open the dropdown like so (image attached below)
When I'm not giving inputValue, i'm unable to access the select components value via e.target.value in handleBlur function which takes care of my form validation.
so how do I solve this problem I'm not able to select any other value after my initial selection what should I do, any help would be appreciated.
I use React.memo to control re-render, but my component still re-rendered.
my code like this:
in my Template Component:
const Template = (props: Props) => {
const { propsValue, data } = props
const [value, setValue] = useState(propsValue)
const handleChange = (value) => {
setValue(value)
props.onChange(value)
}
return (
<div>
{info.type === 'input' && <Input value={value} onChange={(event, val) => handleChange(val) onBlur={(event) => handleBlur()} />
{info.type === 'image' && <Uploader multiple value={value} uploadUrl={data.uploadUrl} onChange={(val) => handleChange(val)} />
{info.type === 'select' && <Select onChange={(val) => handleChange(val)} />
</div>
)
}
const areEqual = (prevProps, nextProps) => {
if (JSON.stringify(prevProps) !== JSON.stringify(nextProps)) {
return false
}
return true
}
export default React.memo(EditTemplate, areEqual)
in my Uploader Component:
const Uploader = props => {
const [value, setValue] = useState(props.value)
let { uploadUrl, multiple } = props
const handleChange = ({ file, fileList }) => {
if (file.status === 'done') {
setValue(fileList)
props.onChange(fileList)
} else {
setValue(fileList)
}
}
return (
<div>
<Upload fileList={value} multiple={multiple} action={uploadUrl} onChange={handleChange} />
</div>
)
}
const areEqual = (prevProps, nextProps) => {
if (JSON.stringify(prevProps) !== JSON.stringify(nextProps)) {
return false
}
return true
}
export default React.memo(Uploader, areEqual)
when I change value in Select Component, the areEqual seems like not work, the address of all images in Upload Component will reload. why...?
the performance like this:
how can I do?
The rerender might be because of the internal state change(setValue(value)). React.memo doesn't prevent rerender caused by a state change.
React.memo only checks for prop changes. If your function component
wrapped in React.memo has a useState or useContext Hook in its
implementation, it will still rerender when state or context change.
Docs
You're passing the onChange param to Uploader like onChange={(val) => handleChange(val)} so this effectively creates new function on each render and probably React.memo gives false positive because of this. Also in Uploader you have props.onChange(fileList) and fileList might also be the reason if it's a different Array instance everytime.
In addition to #Ramesh Reddy's answer, you're calling setValue upon Select changes too, that could also be the reason, since you're using the new value as prop to Uploader.
If this is not the reason, you can add a codesandbox sample with reproduction of the issue.
thanks all very much~I find that the type of my value in Uploader is array, so when Select change ,props.onChange will be lead to change the value, so Uploader will reload.
I add useEffect in Uploader Component like this : useEffect(() => { setValue(formatValue(props.value)) }, [JSON.stringify(props.value)]) ,and then the images will not reload...
I'm creating a search input with a submit button and a clear button. When submit button is clicked, it will trigger a search request with available input value. When clear button is clicked, it will also trigger a search request with empty value. I try to use comparison in useEffect dependency array to trigger effect when the search value is empty to accomodate the clear button.
const Test = ({ doGetData }) => {
const [status, setStatus] = useState(null);
const [activePage, setActivePage] = useState(1);
const [term, setTerm] = useState("");
const handleFilter = () => {
const query = {
page: activePage,
q: term,
active: status
};
doGetData(query);
};
useEffect(() => {
handleFilter();
}, [status, activePage, term === ""]);
const setEmptySearch = () => setTerm("");
const handleInputChange = e => {
const { value } = e.currentTarget;
setTerm(value);
};
return (
<SearchInput
handleFilter={handleFilter}
handleDismissSearch={setEmptySearch}
handleInputChange={handleInputChange}
status={status}
term={term}
/>
);
};
It worked well when I click search button, click the clear button, and also when I manually erase the input.But, the only problem I have is when I type the first letter, it will trigger the search. Can you help me guys?
Just check with an if-statement inside of your useEffect-function:
useEffect(() => {
if(term === "") {
handleFilter();
}
}, [status, activePage, term]);
This question already has answers here:
Make React useEffect hook not run on initial render
(16 answers)
Closed last month.
I'm trying to use the useEffect hook inside a controlled form component to inform the parent component whenever the form content is changed by user and return the DTO of the form content. Here is my current attempt
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const onChange = ({target}) => {
console.log("onChange")
setValue(target.value)
}
return { value, setValue, binding: { value, onChange }}
}
useFormInput.propTypes = {
initialValue: PropTypes.any
}
const DummyForm = ({dummy, onChange}) => {
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [foo, bar])
return (
<div>
<input type="text" {...fooBinding} />
<div>{foo}</div>
<input type="text" {...barBinding} />
<div>{bar}</div>
</div>
)
}
function App() {
return (
<div className="App">
<header className="App-header">
<DummyForm dummy={{value: "Initial"}} onChange={(dummy) => console.log(dummy)} />
</header>
</div>
);
}
However, now the effect is ran on the first render, when the initial values are set during mount. How do I avoid that?
Here are the current logs of loading the page and subsequently editing both fields. I also wonder why I get that warning of missing dependency.
onChange callback
App.js:136 {foo: "Initial", bar: "Initial"}
backend.js:1 ./src/App.js
Line 118: React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
r # backend.js:1
printWarnings # webpackHotDevClient.js:120
handleWarnings # webpackHotDevClient.js:125
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage # webpackHotDevClient.js:190
push../node_modules/sockjs-client/lib/event/eventtarget.js.EventTarget.dispatchEvent # eventtarget.js:56
(anonymous) # main.js:282
push../node_modules/sockjs-client/lib/main.js.SockJS._transportMessage # main.js:280
push../node_modules/sockjs-client/lib/event/emitter.js.EventEmitter.emit # emitter.js:53
WebSocketTransport.ws.onmessage # websocket.js:36
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial"}
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial2"}
You can see this answer for an approach of how to ignore the initial render. This approach uses useRef to keep track of the first render.
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
} else {
// do things after first render
}
});
As for the warning you were getting:
React Hook useEffect has a missing dependency: 'onChange'
The trailing array in a hook invocation (useEffect(() => {}, [foo]) list the dependencies of the hook. This means if you are using a variable within the scope of the hook that can change based on changes to the component (say a property of the component) it needs to be listed there.
If you are looking for something like componentDidUpdate() without going through componentDidMount(), you can write a hook like:
export const useComponentDidMount = () => {
const ref = useRef();
useEffect(() => {
ref.current = true;
}, []);
return ref.current;
};
In your component you can use it like:
const isComponentMounted = useComponentDidMount();
useEffect(() => {
if(isComponentMounted) {
// Do something
}
}, [someValue])
In your case it will be:
const DummyForm = ({dummy, onChange}) => {
const isComponentMounted = useComponentDidMount();
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
if(isComponentMounted) {
console.log("onChange callback")
onChange({foo, bar})
}
}, [foo, bar])
return (
// code
)
}
Let me know if it helps.
I create a simple hook for this
https://stackblitz.com/edit/react-skip-first-render?file=index.js
It is based on paruchuri-p
const useSkipFirstRender = (fn, args) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
console.log('running')
return fn();
}
}, args)
useEffect(() => {
isMounted.current = true
}, [])
}
The first effect is the main one as if you were using it in your component. It will run, discover that isMounted isn't true and will just skip doing anything.
Then after the bottom useEffect is run, it will change the isMounted to true - thus when the component is forced into a re-render. It will allow the first useEffect to render normally.
It just makes a nice self-encapsulated re-usable hook. Obviously you can change the name, it's up to you.
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Example:
useEffectAfterMount(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [count])
I don't understand why you need a useEffect here in the first place. Your form inputs should almost certainly be controlled input components where the current value of the form is provided as a prop and the form simply provides an onChange handler. The current values of the form should be stored in <App>, otherwise how ever will you get access to the value of the form from somewhere else in your application?
const DummyForm = ({valueOne, updateOne, valueTwo, updateTwo}) => {
return (
<div>
<input type="text" value={valueOne} onChange={updateOne} />
<div>{valueOne}</div>
<input type="text" value={valueTwo} onChange={updateTwo} />
<div>{valueTwo}</div>
</div>
)
}
function App() {
const [inputOne, setInputOne] = useState("");
const [inputTwo, setInputTwo] = useState("");
return (
<div className="App">
<header className="App-header">
<DummyForm
valueOne={inputOne}
updateOne={(e) => {
setInputOne(e.target.value);
}}
valueTwo={inputTwo}
updateTwo={(e) => {
setInputTwo(e.target.value);
}}
/>
</header>
</div>
);
}
Much cleaner, simpler, flexible, utilizes standard React patterns, and no useEffect required.