0

I am working with a basic form. The form contains a username field, email field, password field, and a repeated password field. I'm trying to solve state management by throwing the form state in a useReducer. However, I'm not getting the buttonDisabled state to properly calculate out to what it should be. The goal is for the button to be disabled on load, and then when all fields are not empty, and when the password field and the repeated password fields match, the button will enable.

SignUpReducer.js

    userName: "",
    email: "",
    password: "",
    repeatedPassword: "",
    buttonDisabled: true
}


export const formReducer = (state, action) => {
    console.log(state, "STATE");
    console.log(state.buttonDisabled, "BUTTON DISABLED?");
    switch(action.type) {
        case "Handle Input Text":
            return {
                [action.field]: action.payload,
                buttonDisabled: state.password === state.repeatedPassword,
            };
        default: 
            return state;
    }
}

SignUpPage.js

import React from "react";
import { useReducer } from "react";
import { formReducer, initialState } from "./SignUpReducer";





const SignUpPage = () => {
    const [state, dispatch] = useReducer(formReducer, initialState);

    const handleTextChange = (e) => {
        dispatch({
            type: "Handle Input Text",
            field: e.target.name,
            payload: e.target.value,
        });
    }

    return (
        <>
        <h1>Sign Up</h1>
        <label htmlFor="username">Username</label>
        <input type="text" name="userName" value={state.userName} onChange={handleTextChange} id="username"/>
        <label htmlFor="email">E-mail</label>
        <input type="text" name="email" value={state.email} onChange={handleTextChange} id="email"/>
        <label htmlFor="password">Password</label>
        <input name="password" value={state.password} onChange={handleTextChange} type="password" id="password"/>
        <label htmlFor="repeatedPassword">Repeat Password</label>
        <input name="repeatedPassword" value={state.repeatedPassword} onChange={handleTextChange} type="password" id="repeatedPassword"/>
        <button className="btn-primary" disabled={state.buttonDisabled}>Sign Up</button>
        </>
    )
};
export default SignUpPage;
  • Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. – Community Sep 13 '22 at 10:48

1 Answers1

0

You'll need to update the fields state before checking the disabled status of the button:

const { useReducer } = React;

const initialState = {    
  userName: "",
  email: "",
  password: "",
  repeatedPassword: "",
  buttonDisabled: true
}

const formReducer = (state, action) => {
  switch(action.type) {
    case "Handle Input Text":
      const { buttonDisabled, ...fieldsState } = state;
    
      // update the field
      const form = {
        ...fieldsState,
        [action.field]: action.payload,
      };
      
      // compute disabled with new values of fields
      const disabled =
        !Object.values(form).every(Boolean) // are all values filled
        || form.password !== form.repeatedPassword; // are the password identical
      
      return {
        ...form,
        buttonDisabled: disabled
      };
    default: 
      return state;
  }
}

const SignUpPage = () => {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleTextChange = (e) => {
    dispatch({
      type: "Handle Input Text",
      field: e.target.name,
      payload: e.target.value,
    });
  }

  return (
    <div>
      <h1>Sign Up</h1>
      <label htmlFor="username">Username</label>
      <input type="text" name="userName" value={state.userName} onChange={handleTextChange} id="username"/>
      <label htmlFor="email">E-mail</label>
      <input type="text" name="email" value={state.email} onChange={handleTextChange} id="email"/>
      <label htmlFor="password">Password</label>
      <input name="password" value={state.password} onChange={handleTextChange} type="password" id="password"/>
      <label htmlFor="repeatedPassword">Repeat Password</label>
      <input name="repeatedPassword" value={state.repeatedPassword} onChange={handleTextChange} type="password" id="repeatedPassword"/>
      <button className="btn-primary" disabled={state.buttonDisabled}>Sign Up</button>
    </div>
  )
};

ReactDOM.createRoot(root)
  .render(<SignUpPage />)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>

A better option would be to remove the disabledButton state from the reducer, because it's derived from the form state. You can also minimize the reducer by passing it an update object, and spreading it over the existing state:

const { useReducer } = React;

const initialState = {    
  userName: "",
  email: "",
  password: "",
  repeatedPassword: "",
}

const formReducer = (state, update) => ({
  ...state,
  ...update,
});

const SignUpPage = () => {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleTextChange = (e) => {
    dispatch({ [e.target.name]: e.target.value });
  }
  
  const disabled =
    !Object.values(state).every(Boolean) // are all values filled
    || state.password !== state.repeatedPassword; // are the password identical

  return (
    <div>
      <h1>Sign Up</h1>
      <label htmlFor="username">Username</label>
      <input type="text" name="userName" value={state.userName} onChange={handleTextChange} id="username"/>
      <label htmlFor="email">E-mail</label>
      <input type="text" name="email" value={state.email} onChange={handleTextChange} id="email"/>
      <label htmlFor="password">Password</label>
      <input name="password" value={state.password} onChange={handleTextChange} type="password" id="password"/>
      <label htmlFor="repeatedPassword">Repeat Password</label>
      <input name="repeatedPassword" value={state.repeatedPassword} onChange={handleTextChange} type="password" id="repeatedPassword"/>
      <button className="btn-primary" disabled={disabled}>Sign Up</button>
    </div>
  )
};

ReactDOM.createRoot(root)
  .render(<SignUpPage />)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>

If you want all logic to be separated from the view, you can extract it by creating a custom hook:

const { useReducer, useCallback } = React;

const initialState = {    
  userName: "",
  email: "",
  password: "",
  repeatedPassword: "",
}

const formReducer = (state, update) => ({
  ...state,
  ...update,
});

const useLoginForm = () => {
  const [form, dispatch] = useReducer(formReducer, initialState);

  const handleTextChange = useCallback(e => {
    dispatch({ [e.target.name]: e.target.value });
  }, []);
  
  const buttonDisabled =
    !Object.values(form).every(Boolean) // are all values filled
    || form.password !== form.repeatedPassword; // are the password identical
    
  return {
    form,
    handleTextChange,
    buttonDisabled
  };
};

const SignUpPage = () => {
  const {
    form,
    handleTextChange,
    buttonDisabled
  } = useLoginForm();

  return (
    <div>
      <h1>Sign Up</h1>
      <label htmlFor="username">Username</label>
      <input type="text" name="userName" value={form.userName} onChange={handleTextChange} id="username"/>
      <label htmlFor="email">E-mail</label>
      <input type="text" name="email" value={form.email} onChange={handleTextChange} id="email"/>
      <label htmlFor="password">Password</label>
      <input name="password" value={form.password} onChange={handleTextChange} type="password" id="password"/>
      <label htmlFor="repeatedPassword">Repeat Password</label>
      <input name="repeatedPassword" value={form.repeatedPassword} onChange={handleTextChange} type="password" id="repeatedPassword"/>
      <button className="btn-primary" disabled={buttonDisabled}>Sign Up</button>
    </div>
  )
};

ReactDOM.createRoot(root)
  .render(<SignUpPage />)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209