I've got a custom Input control, which I'd like to be uncontrolled. I don't care what the user writes or where this is stored. However, I need to know if the input has some value or not, because depending on that it will have an appearance or another (such as a floating label).
Since it's not possible to check purely by CSS if an input has a value or not (because the attribute value does not get updated when the user types and changes its value), the only way to know if it has a value or not is programatically.
If I use useState to keep a local state of its value just to know if it has some (though it keeps being uncontrolled), whenever some external script changes the value of the input using ref (for example, react-hook-forms reset function), it won't get updated because the state won't notice the change, and thus the input will be empty but its state will think it still has a value.
As it's not a possibility, then, I wanted to access and watch for the ref .value changes. However, it doesn't change (or, at least, it doesn't notify the change). If I access the value prop directly, it has the correct value, but it doesn't trigger the useEffect although what I'm using as a dependency is the value and not the ref itself.
const [hasValue, setHasValue] = useState(false);
useEffect(() => {
setHasValue(!!inputRef?.current?.value);
}, [inputRef?.current?.value]);
Here is Codesandbox a sample of what I'm referring.
Is there any way to check the actual value of the input inside my custom component without having to send a value prop from the parent, which is able to react to external changes by
ref, too? So, no matter how the input value changes, it's aware of it.
You could use Inder's suggestion with vanilla javascript or you could trigger your useEffect with afterValue, which gets set onChange.
import React, { useEffect, useRef, useState } from "react";
const Input = () => {
const inputRef = useRef(null);
const [afterValue, setAfterValue] = useState("");
const [hasValue, setHasValue] = useState(false);
useEffect(() => {
setHasValue(!!inputRef?.current?.value);
}, [afterValue]);
return (
<div>
<input
ref={inputRef}
onChange={({ target: { value } }) =>
setTimeout(() => setAfterValue(value), 1000)
}
/>
<br />
<br />
<div>Has value: {hasValue.toString()}</div>
<div>Actual value: {inputRef?.current?.value}</div>
<div>After value: {afterValue}</div>
</div>
);
};
export default Input;
Related
I'm trying to give a value of the props note to the TextField but if I use TextInput the result is undefined and if I use the note props, I cannot change the value of the TextField afterward.
When I console log both of the value, they render twice as shown in the image.
I tried useEffect also to set the state.
export default function ProfileNote({ contact }) {
const { note, id } = contact;
const [textInput, setTextInput] = useState(note);
console.log(`textInput: ${textInput}`)
console.log(`note: ${note}`)
const handleTextInputChange = event => {
setTextInput(event.target.value);
};
return (
<FormProvider onSubmit={handleSubmit(onSubmit)}>
<TextField
name="notes"
label="Notes"
onChange={handleTextInputChange}
value={textInput}
/>
</FormProvider>
);
}
Image of the console.log
Thank you.
I assume that the code that calls ProfileNote receives the contact asynchronously from a database or an API. When the asynchronous call is started, the value of contact.note is still undefined, but your component is still rendered.
This undefined value is now used as the initial value for textField. Once the asynchronous function call returns with a value (e.g. ddfdf), the textField value will not be changed since it is already initialized.
You can do one of two things in order to fix this, dependent on how you want your UI to behave:
prevent rendering the ProfileNote until the data is available:
<>
{ contact != null && <ProfileNote contact={contact}/> }
</>
continuously apply changes to contact.note to textField, potentially overwriting user input if contact changes while the user is editing:
useEffect(() => setTextInput(note), [note]);
I have this issue with state update order. There's a data source, which contains some values stored by keys, and the values are constantly updated via WebSocket. And I've made a custom hook, that sets initial value from data source and subscribes to socket updates - and calls setState on any relevant update.
Then I have a form with a number input which uses the current data source value as initial form input value. For example, on component render the data source provides value "345", and then user can edit it (increase/decrease).
Now, when user selects a different data source, the input value should be reset to current data source value again - but only as a result of explicit user action - pressing a data source toggle button. Resetting it on each socket change will make the form input unusable.
The problem is when I change the data source key, the value returned from the custom hook is the old one (it only changes in useEffect).
Here's code example:
import React, { useEffect, useState } from "react";
// some data source, which has initial values and is constantly updated from socket
const data = {
one: 12345,
two: 23456,
};
// a simplified version of hook, which gets current value
// and subscribes to socket updates
const useMyHook = (key) => {
const [state, setState] = useState(data[key]);
useEffect(() => {
// set current state
setState(data[key]);
// subscribes/unsubscribes to socket updates, omitted for simplicity
// ...
}, [key]);
return state;
};
export default function App() {
const [dataSource, setDataSource] = useState("one");
const dataSourceValue = useMyHook(dataSource);
const [fieldValue, setFieldValue] = useState(dataSourceValue);
const handleDataSourceToggle = (e) => {
e.preventDefault();
setDataSource(dataSource === "one" ? "two" : "one");
};
const handleUserInput = (e) => {
setFieldValue(e.target.value);
};
// input value reset - please note, that I cannot use dataSourceValue as a dependency here
// because I don't want to lose the value entered by user - only when he explicitly changes the data source
useEffect(() => {
setFieldValue(dataSourceValue);
}, [dataSource]);
return (
<div className="App">
<h1>Custom Hook Example</h1>
<form>
<p>
<input type="number" value={fieldValue} onChange={handleUserInput} />
</p>
<p>
Data source: <strong>{dataSource}</strong>
</p>
<p>
Data source value: <strong>{dataSourceValue}</strong>
</p>
<button onClick={handleDataSourceToggle}>Change data source</button>
</form>
</div>
);
}
This example is simplified on purpose, in reality the data source is switched in some external component, the current component receives it from React context prop.
How do I fix this?
Here's the sandbox: https://codesandbox.io/s/elated-brook-r9iubv?file=/src/App.js
Now, when user selects a different data source, the input value should be reset to current data source value again - but only as a result of explicit user action - pressing a data source toggle button. Resetting it on each socket change will make the form input unusable.
This isn't something that React can guess, you have to say (program) explicitly which events should change fieldValue, for instance by adding another setFieldValue in the handleDataSourceToggle callback.
You should also avoid this:
// input value reset - please note, that I cannot use dataSourceValue as a dependency here
// because I don't want to lose the value entered by user - only when he explicitly changes the data source
useEffect(() => {
setFieldValue(dataSourceValue);
}, [dataSource]);
You will get eslint warnings because this is not how effects should be used. If you feel like you need to omit some dependencies, it most likely means that you should write your code differently instead.
In this particular case, I don't think you need this effect at all.
I'm doing a feature using react, which saves text from input and automatically updates to local storage, I want even if I refresh the page the text in the input stays and the state doesn't reset from scratch. Thanks for helping me, have a nice day. please give me an the demo in the following codesandbox codesandbox link. one more time thank you very much
First you want to store your input's value in react state
const [value, setValue] = useState("");
When the input changes, write the change to react state, and also localStorage
const handleChange = (e) => {
setValue(e.target.value);
localStorage.setItem("inputValue", e.target.value);
};
When the component is mounted, we want to set the initial state of the input to the value in localStorage
useEffect(() => {
setValue(localStorage.getItem("inputValue"));
}, []);
Finally, hook the handler to onChange, and use the React state as the form value
<input
value={value}
onChange={handleChange}
/>
See: https://codesandbox.io/s/react-input-not-reload-after-refreshing-forked-q84r8?file=/demo.js
I have a functional component that initializes a state with useState, then this state is changed via an input field.
I then have a useEffect hook simulating componentWillUnmount so that, before the component unmounts, the current, updated state is logged to the console. However, the initial state is logged instead of the current one.
Here is a simple representation of what I am trying to do (this is not my actual component):
import React, { useEffect, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
useEffect(() => {
return () => {
console.log(text);
}
}, [])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
I initialize the state as "initial." Then I use the input field to change the state, say I type in "new text." However, when the component in unmounted, "initial" is logged to the console instead of "new text."
Why does this happen? How can I access the current updated state on unmount?
Many thanks!
Edit:
Adding text to useEffect dependency array doesn’t solve my problem because in my real-world scenario, what I want to do is to fire an asynchronous action based on the current state, and it wouldn’t be efficient to do so everytime the “text” state changes.
I’m looking for a way to get the current state only right before the component unmounts.
You've effectively memoized the initial state value, so when the component unmounts that value is what the returned function has enclosed in its scope.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
In order to get the latest state when the cleanup function is called then you need to include text in the dependency array so the function is updated.
Effect hook docs
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values. While passing [] as the second
argument is closer to the familiar componentDidMount and
componentWillUnmount mental model, there are usually better solutions
to avoid re-running effects too often.
This means the returned "cleanup" function still only accesses the previous render cycle's state and props.
EDIT
useRef
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.
...
It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
Using a ref will allow you to cache the current text reference that can be accessed within the cleanup function.
/EDIT
Component
import React, { useEffect, useRef, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
// #1 ref to cache current text value
const textRef = useRef(null);
// #2 cache current text value
textRef.current = text;
useEffect(() => {
console.log("Mounted", text);
// #3 access ref to get current text value in cleanup
return () => console.log("Unmounted", text, "textRef", textRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
console.log("current text", text);
return () => {
console.log("previous text", text);
}
}, [text])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
With the console.log in the returned cleanup function you'll notice upon each change in the input the previous state is logged to console.
In this demo I've logged current state in the effect and previous state in the cleanup function. Note that the cleanup function logs first before the current log of the next render cycle.
I'm trying to build an input component using React Hooks that hits a remote server to save an updated value on component unmount only.
The remote server call is expensive, so I do not want to hit the server every time the input updates.
When I use the cleanup hook in useEffect, I am required to include the input value in the effect dependency array, which makes the remote API call execute on each update of the input value. If I don't include the input value in the effect dependency array, the updated input value is never saved.
Here is a code sandbox that shows the problem and explains the expected outcome: https://codesandbox.io/s/competent-meadow-nzkyv
Is it possible to accomplish this using React hooks? I know it defies parts of the paradigm of hooks, but surely this is a common-enough use case that it should be possible.
You can use a ref to capture the changing value of your text, then you can reference it in another useEffect hook to save the text:
const [text, setText] = useState("");
const textRef = React.useRef(text);
React.useEffect( () => {
textRef.current = text;
}, [text])
React.useEffect( () => {
return () => doSomething(textRef.current)
}, [])
thedude's approach is right. Tweaked it a bit, for this particular usecase as input ref is always same :
function SavedInput() {
const inputEl = useRef(null);
React.useEffect(() => {
return () => {
save(inputEl.current.value);
};
}, []);
return (
<div>
<input ref={inputEl} />
</div>
);
}
By this way you'll avoid re-render as you are not setting any state.