Recoil icon indicating copy to clipboard operation
Recoil copied to clipboard

Setter in selector forces the input cursor to jump to end of input field on change event

Open gregordotjs opened this issue 4 years ago • 6 comments

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...

gregordotjs avatar Jul 19 '20 08:07 gregordotjs

@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.

wsmd avatar Jul 20 '20 19:07 wsmd

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 avatar Nov 25 '20 21:11 drarmstr

@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

xuzhanhh avatar Dec 15 '20 04:12 xuzhanhh

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 avatar Apr 05 '21 20:04 bbansalWolfPack

@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>
  );
}

NickAkhmetov avatar Apr 05 '21 20:04 NickAkhmetov

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

RidhwanDev avatar Dec 04 '21 18:12 RidhwanDev