facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.6k stars 1.19k forks source link

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

Open gregordotjs opened 4 years ago

gregordotjs commented 4 years ago

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

wsmd commented 4 years ago

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

drarmstr commented 3 years ago

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.

xuzhanhh commented 3 years ago

@drarmstr Text input case will definitely happen in react@16.8.3. 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

bbansalWolfPack commented 3 years ago

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?

NickAkhmetov commented 3 years ago

@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>
  );
}
RidhwanDev commented 2 years ago

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