Search code examples
reactjsreact-hooksrecoiljs

Multiple form values in one react recoil atom override each other


Is there any way to hold multiple form input values in one React Recoil atom? I keep trying to add 2 form field values, but they just override each other.

I have a registration form which has 2 fields; email and phone.

My (simplified) form component looks like so;

import { atom, useSetRecoilState, useRecoilValue } from 'recoil';

const registerAtom = atom({
    key: 'register',
    default: [],
});

function Registration() {
    const setEmail = useSetRecoilState(registerAtom);
    const email = useRecoilValue(registerAtom);

    const setPhone = useSetRecoilState(registerAtom);
    const phone = useRecoilValue(registerAtom);

    return (
        <>
            <form>
                <input name="email" type="text" className="form-control" value={email} onChange={e => setEmail(e.target.value)} placeholder="Email Address" />
                <input name="phone" type="text" className="form-control" value={phone} onChange={e => setPhone(e.target.value)} placeholder="Phone Number" />
            </form>
        </>
    )
}

Solution

  • If you are certain that you will never need to read or write the email and phone states independently, a simple approach is to use a single atom with an object value (this is equivalent to using React's useState hook with an object value):

    import {atom} from 'recoil';
    
    const contactInfoState = atom({
      key: 'contactInfo',
      default: {
        email: '',
        phone: '',
      },
    });
    

    Then, use like this (updating the entire object every time):

    import {useRecoilState} from 'recoil';
    
    function Registration () {
      const [{email, phone}, setContactInfo] = useRecoilState(contactInfoState);
      
      return (
        <form>
          <input
            type="text"
            value={email}
            onChange={ev => setContactInfo({email: ev.target.value, phone})}
            placeholder="Email Address"
          />
          <input
            type="text"
            value={phone}
            onChange={ev => setContactInfo({email, phone: ev.target.value})}
            placeholder="Phone Number"
          />
        </form>
      )
    }
    

    However, the idiomatic way to do this (and where Recoil becomes more powerful) is composition of atoms using a selector, which can provide a way for reading and writing the values together (just like in the example above), yet still allows for reading and writing them independently using their atoms:

    import {atom, DefaultValue, selector} from 'recoil';
    
    const emailState = atom({
      key: 'email',
      default: '',
    });
    
    const phoneState = atom({
      key: 'phone',
      default: '',
    });
    
    const contactInfoState = selector({
      key: 'contactInfo',
      get: ({get}) => {
        // get values from individual atoms:
        const email = get(emailState);
        const phone = get(phoneState);
        // then combine into desired shape (object) and return:
        return {email, phone};
      },
      set: ({set}, value) => {
        // in a Reset action, the value will be DefaultValue (read more in selector docs):
        if (value instanceof DefaultValue) {
          set(emailState, value);
          set(phoneState, value);
          return;
        }
        // otherwise, update individual atoms from new object state:
        set(emailState, value.email);
        set(phoneState, value.phone);
      },
    });
    

    Here's a complete and self-contained example in a snippet, which you can run on this page to verify that it works:

    Note: It uses the UMD versions of React, ReactDOM, and Recoil, so they are exposed globally using those names instead of using import statements.

    <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
    <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/[email protected]/umd/recoil.min.js"></script>
    <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
    
    <div id="root"></div>
    
    <script type="text/babel" data-type="module" data-presets="react">
    
    const {
      atom,
      DefaultValue,
      RecoilRoot,
      selector,
      useRecoilValue,
      useSetRecoilState,
    } = Recoil;
    
    const emailState = atom({
      key: 'email',
      default: '',
    });
    
    const phoneState = atom({
      key: 'phone',
      default: '',
    });
    
    const contactInfoState = selector({
      key: 'contactInfo',
      get: ({get}) => {
        const email = get(emailState);
        const phone = get(phoneState);
        return {email, phone};
      },
      set: ({set}, value) => {
        if (value instanceof DefaultValue) {
          set(emailState, value);
          set(phoneState, value);
          return;
        }
        set(emailState, value.email);
        set(phoneState, value.phone);
      },
    });
    
    function Registration () {
      const {email, phone} = useRecoilValue(contactInfoState);
      const setEmail = useSetRecoilState(emailState);
      const setPhone = useSetRecoilState(phoneState);
      
      return (
        <form>
          <input
            type="text"
            value={email}
            onChange={ev => setEmail(ev.target.value)}
            placeholder="Email Address"
          />
          <input
            type="text"
            value={phone}
            onChange={ev => setPhone(ev.target.value)}
            placeholder="Phone Number"
          />
        </form>
      )
    }
    
    function DisplayState () {
      const email = useRecoilValue(emailState);
      const phone = useRecoilValue(phoneState);
      return (
        <pre>
          <code>{JSON.stringify({email, phone}, null, 2)}</code>
        </pre>
      );
    }
    
    function Example () {
      return (
        <RecoilRoot>
          <Registration />
          <DisplayState />
        </RecoilRoot>
      );
    }
    
    ReactDOM.render(<Example />, document.getElementById('root'));
    
    </script>