Recoil
Recoil copied to clipboard
Setter in selector forces the input cursor to jump to end of input field on change event
I'm using a selector to get and set values via useRecoilState. Here are my atom and selector:
// Atom for lng and lat values, not really used anywhere but in the selector below
export const locationStateAtom = atom({
key: "locationStateAtom",
default: {
lng: null,
lat: null
}
});
// locationState is selector that gets async values from geolocation API (mocked here) if values are not set, otherwise returns the latest set values.
// to set the vales of this selector I needed locationStateAtom defined above
export const locationState = selector({
key: "locationState",
get: async ({ get }) => {
try {
const location = get(locationStateAtom);
if (!location.lng || !location.lat) {
const { lng, lat } = await geoLocation;
return { lng, lat };
}
return location;
} catch (e) {
console.log(e.message);
}
},
set: ({ set }, newValue) => {
set(locationStateAtom, newValue);
}
});
And here's how I'm using it:
function LocationForm() {
const [location, setLocation] = useRecoilState(locationState);
const resetLocation = useResetRecoilState(locationState);
const handleOnChange = e => {
e.preventDefault();
setLocation(prevState => ({
...prevState,
[e.target.name]: e.target.value
}));
};
return (
<>
<h3>Location form</h3>
{location && (
<form>
<input
type="text"
name="lat"
placeholder="lat"
value={location.lat}
onChange={handleOnChange}
/>
<input
type="text"
name="lng"
placeholder="lng"
value={location.lng}
onChange={handleOnChange}
/>
<button type="button" onClick={e => resetLocation()}>
Reset location
</button>
</form>
)}
</>
);
}
So if you want to change the coords (i.e. start typing in the input) the cursor will automatically jump at the end. Here's a working example: https://stackblitz.com/edit/react-exchjb?file=index.js
Seems like a bug or perhaps I'm not using the Recoil lib correctly...
@javascrewpt what's happening there is normal due to Suspense
.
The reason the cursor is jumping to the end is because Suspense
is re-mounting the component tree entirely after you update the selector value; the locationState
selector is asynchronous and will trigger Suspense to do what is supposed to (show the fallback
state and wait for your component to resolve) any time it gets updated. You can't really see the fallback because the the selector/promise resolve immediately.
There are probably many ways to get around this, but it all depends on how you want to restructure you code (e.g. adding local state for the input values, etc.).
P.S. since the locationState
selector is asynchronous, you should look into useRecoilValueLoadable
and useRecoilStateLoadable
as ways to work with this kind of selectors.
HTML text inputs manage their own state. So, using Recoil state for them causes the two to conflict and the cursor behaviour that you observed. React supports using React state to make text inputs a controlled component with some magic to workaround this issue. So, one option is to use an abstraction to map the Recoil state to React state to use with the text input.
If anyone wants to help add this magic to Recoil, that would be wonderful.
@drarmstr Text input case will definitely happen in [email protected]. here's demo: https://codesandbox.io/s/silly-hofstadter-u8dh5?file=/src/App.js . So it would be better to add a hint that recoil needs react@^16.13.1 on https://recoiljs.org/docs/introduction/getting-started
I am also seeing this issue. I am updating my recoil state when user types (on each keystroke). If I click in middle of text, then cursor auto jumps to end. Anyone knows whats wrong? and how to fix it?
@bbansalWolfPack I was able to work around this behavior by using a normal react useState to manage the input, with a useEffect updating the recoil atom whenever the input value updates.
import { useEffect, useState } from 'react';
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';
const dataAtom = atom<string>({
key: 'dataAtom',
default: '',
});
const RecoilStateDisplay = () => {
const data = useRecoilValue(dataAtom);
return (
<div>
Recoil State value: {data}
</div>
)
}
const ReactStateComponent = () => {
const setDataAtom = useSetRecoilState(dataAtom);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
setDataAtom(inputValue);
}, [inputValue]);
return (
<div>
<input type="text" value={inputValue} onChange={event => setInputValue(event.target.value)}/>
<div>
State value: {inputValue}
</div>
</div>
);
}
I have a very similar issue, but mine loses focus of the text input entirely after 1 keystroke. The above solution did not help.. I also thought this was because of the way I was creating atoms, and restructured my entire code to use atoms family but made no difference