reactjsnext.jsuse-statereact-hook-formfaunadb

react-hook-form and useState (toggle)


I have the following use case:

A user wants to toggle whether a profile is active or not.

Configuration: Next.js, Fauna DB, react-hook-form

I use useState to change the state on the toggle, and react-hook-forms to send other values to my Fauna database and the state from the toggle. I would like the toggle to have the state from the database, and when the user toggles it followed by a press on the submit button, I would like to change the state in the database.

I cannot seem to send the right state back to the database when I'm toggling it.

Main component:

export default function Component() {
 const [status, setStatus] = useState(
    userData?.profileStatus ? userData.profileStatus : false
  );

const defaultValues = {
    profileStatus: status ? userData?.profileStatus : false
  };

const { register, handleSubmit } = useForm({ defaultValues });

  const handleUpdateUser = async (data) => {

    const {
      profileStatus
    } = data;
    try {
      await fetch('/api/updateProfile', {
        method: 'PUT',
        body: JSON.stringify({
          profileStatus
        }),
        headers: {
          'Content-Type': 'application/json'
        }
      });
      alert(`submitted data: ${JSON.stringify(data)}`);
    } catch (err) {
      console.error(err);
    }
  };

return (
  <div>
    <form onSubmit={handleSubmit(handleUpdateUser)}>
      <Toggle status={status} setStatus={setStatus} />
      <button type="submit">
        Save
      </button>
    </form>
  </div>
 )
}

Toggle component:

import { Switch } from '@headlessui/react';

function classNames(...classes) {
  return classes.filter(Boolean).join(' ');
}

export default function Toggle({status , setStatus}) {

  return (
    <Switch.Group as="div" className="flex items-center justify-between">
      <span className="flex-grow flex flex-col">
        <Switch.Label
          as="span"
          className="text-sm font-medium text-gray-900"
          passive
        >
          Profilstatus
        </Switch.Label>
        <Switch.Description as="span" className="text-sm text-gray-500 w-44">
          Her sætter du om din profil skal være aktiv eller inaktiv.
        </Switch.Description>
      </span>
      <Switch
        checked={status}
        onChange={setStatus}
        className={classNames(
          status ? 'bg-blue-600' : 'bg-gray-200',
          'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
        )}
      >
        <span
          aria-hidden="true"
          className={classNames(
            status ? 'translate-x-5' : 'translate-x-0',
            'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
          )}
        />
      </Switch>
    </Switch.Group>
  );
}

updateProfile.js

import { updateProfileInfo } from '@/utils/Fauna';
import { getSession } from 'next-auth/react';

export default async (req, res) => {
  const session = await getSession({ req });
  if (!session) return res.status(401);

  const userId = session.user.id;
  if (req.method !== 'PUT') {
    return res.status(405).json({ msg: 'Method not allowed' });
  }

  const {
    profileStatus,
    image,
    about,
    preferences,
    socialmedia
   } = req.body;
  try {
    const updated = await updateProfileInfo(
      userId,
      profileStatus,
      image,
      about,
      preferences,
      socialmedia
    );
    return res.status(200).json(updated);
  } catch (err) {
    console.error(err);
    res.status(500).json({ msg: 'Something went wrong.' });
  }
  res.end();
};

Fauna.js

const updateProfileInfo = async (
  userId,
  profileStatus,
  image,
  about,
  preferences,
  socialmedia
) => {
  return await faunaClient.query(
    q.Update(q.Ref(q.Collection('users'), userId), {
      data: {
        profileStatus,
        image,
        about,
        preferences,
        socialmedia
      }
    })
  );
};

module.exports = {
  updateProfileInfo

}

Can you guys see what I'm doing wrong?


Solution

  • I made a small sandbox to demonstrate how you could implement your use case using react-hook-form.

    The reason it isn't working is, that you never update react-hook-form's internal state when toggling the switch, you only update your useState. So when you call handleUpdateUser the data that is passed as the argument is the initial data you set via defaultValues.

    Actually there is no need to use useState here, as you could just use react-hook-form's internal form state. For this to work you have to use the <Controller /> component react-hook-form provides as the <Switch /> component from Headless UI @headlessui/react is an external controlled component which doesn't expose a ref prop for an actual <input /> element (<Switch /> uses a <button /> instead of an <input /> element). You can find more info here.

    This way to you can also make your <Toggle /> more generic for a reuse by providing a value and onChange prop instead of status and setStatus. But of course you could also use these names still. The <Controller /> will provide a value and onChange prop on the field object which i spread on the <Toggle /> component.

    In your example it isn't clear how your <Component /> component will receive the initial userData. I assumed you'd make an api request and so i put it in a useEffect. To update the form state after the api call finished you have to use the reset method react-hook-form provides. If you only render <Component /> when the userData is already loaded you can omit this step and just pass the result to the defaultValues to useForm.

    I mocked the api calls with a simple Promise, but you should get the idea.

    Component.js

    import { useEffect } from "react";
    import { Controller, useForm } from "react-hook-form";
    import Toggle from "./Toggle";
    
    // Server Mock
    let databaseState = {
      profileStatus: true
    };
    
    const getUserData = () => Promise.resolve(databaseState);
    const updateUserData = (newState) => {
      databaseState = newState;
    
      return Promise.resolve(newState);
    };
    
    function Component() {
      const { control, reset, handleSubmit } = useForm({
        defaultValues: { profileStatus: false }
      });
    
      useEffect(() => {
        const loadData = async () => {
          const result = await getUserData();
    
          reset(result);
        };
    
        loadData();
      }, [reset]);
    
      const handleUpdateUser = async (data) => {
        try {
          const result = await updateUserData(data);
    
          console.log(result);
        } catch (err) {
          console.error(err);
        }
      };
    
      return (
        <div>
          <form onSubmit={handleSubmit(handleUpdateUser)}>
            <Controller
              control={control}
              name="profileStatus"
              render={({ field: { ref, ...field } }) => <Toggle {...field} />}
            />
            <button type="submit">Save</button>
          </form>
        </div>
      );
    }
    

    Toggle.js

    import { Switch } from "@headlessui/react";
    
    function classNames(...classes) {
      return classes.filter(Boolean).join(" ");
    }
    
    export default function Toggle({ value, onChange }) {
      return (
        <Switch.Group as="div" className="flex items-center justify-between">
          <span className="flex-grow flex flex-col">
            <Switch.Label
              as="span"
              className="text-sm font-medium text-gray-900"
              passive
            >
              Profilstatus
            </Switch.Label>
            <Switch.Description as="span" className="text-sm text-gray-500 w-44">
              Her sætter du om din profil skal være aktiv eller inaktiv.
            </Switch.Description>
          </span>
          <Switch
            checked={value}
            onChange={onChange}
            className={classNames(
              value ? "bg-blue-600" : "bg-gray-200",
              "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
            )}
          >
            <span
              aria-hidden="true"
              className={classNames(
                value ? "translate-x-5" : "translate-x-0",
                "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
              )}
            />
          </Switch>
        </Switch.Group>
      );
    }
    

    Edit inspiring-moon-0lqhdg