cris
cris

Reputation: 225

toggling multiple states in React using useReducer

I am creating a sign-up page where the user can toggle between showing and hiding the passwords in the input field. There are 2 password input fields - create password and reenter password field. Each input fields has a button to toggle between showing and hiding the password. Since the toggle changes multiple states in one element, I decided to use the useReducer hook instead of useState hook. While my current code works as far as toggling the input type to show and hide the password the problem is that the toggle buttons in each element mutates the boolean state globally therefore simultaneously showing/hiding the password in each password fields everytime I click the show/hide button. What I want to achieve is for the user to inpedently toggle Show/Hide password from each input fields so that when the user shows the password in the "create password" field the "reenter password" field's password remains hidden(and vice-versa) until the user toggles the state of that element.

Please help me understand why my code is mutating the initial state of my component and a fix would be appreciated.

Here is the simplified version of my code:

Props = {
  inputType?: string;
  isShowPassword?: boolean;
  dispatch?: Dispatch<{ type: string; payload: PayloadType }>;
};

type PayloadType = {
  inputType: string;
  isShowPassword: boolean;
};

type ActionTypes = { type: string; payload: PayloadType };

const defaultState: PayloadType = {
  inputType: "password",
  isShowPassword: false,
};
type Reducer<S, A> = (prevState: S, action: A) => S;

const reducerFunction = (state: PayloadType, action: ActionTypes) => {
  switch (action.type) {
    case "SHOW":
      return {
        ...state,
        inputType: action.payload.inputType,
        isShowPassword: action.payload.isShowPassword,
      };
    case "HIDE":
      return {
        ...state,
        inputType: action.payload.inputType,
        isShowPassword: action.payload.isShowPassword,
      };
  }
  return state;
};

const Login = () => {
  const [{ inputType, isShowPassword }, dispatch] = useReducer<
    Reducer<any, any>
  >(reducerFunction, defaultState);
  const [isAlreadyMember, setIsAlreadyMember] = useState(true);

  return (
    <section>
      <div>
        <Signup
          dispatch={dispatch}
          inputType={inputType}
          isShowPassword={isShowPassword}
          setIsAlreadyMember={setIsAlreadyMember}
        />
      </div>
    </section>
  );
};

const Signup = ({
  inputType,
  dispatch,
  isShowPassword,
  setIsAlreadyMember,
}: Props) => {
  return (
    <div>
      <form action="">
        <input type="email" name="email" placeholder="email" />
        <div>
          <input
            type={inputType}
            name="password"
            placeholder="create password"
          />
          //toggle show/hide password
          {isShowPassword === false ? (
            <button
              onClick={(e) => {
                e.preventDefault();
                return dispatch!({
                  type: "SHOW",
                  payload: {
                    inputType: "text",
                    isShowPassword: true,
                  },
                });
              }}
              className="bg-none text-xs  absolute right-0 p-3 bottom-0"
            >
              Show
            </button>
          ) : (
            <button
              onClick={(e) => {
                e.preventDefault();
                return dispatch!({
                  type: "HIDE",
                  payload: {
                    inputType: "password",
                    isShowPassword: false,
                  },
                });
              }}
            >
              Hide
            </button>
          )}
        </div>
        <div>
          <input
            type={inputType}
            name="password"
            placeholder="reenter password"
          />
          {isShowPassword === false ? (
            <button
              onClick={(e) => {
                e.preventDefault();
                return dispatch!({
                  type: "SHOW",
                  payload: {
                    inputType: "text",
                    isShowPassword: true,
                  },
                });
              }}
            >
              Show
            </button>
          ) : (
            <button
              onClick={(e) => {
                e.preventDefault();
                return dispatch!({
                  type: "HIDE",
                  payload: {
                    inputType: "password",
                    isShowPassword: false,
                  },
                });
              }}
            >
              Hide
            </button>
          )}
        </div>
        <button
          onClick={(e) => e.preventDefault()}
          className="primary-btn w-full mt-5"
        >
          Sign Up
        </button>
      </form>
    </div>
  );
};

export default Signup;

Upvotes: 1

Views: 599

Answers (1)

anon
anon

Reputation:

I'm very confident saying that the components are not behaving in the way you expect because of the defaultState definition.

You have two inputs but only one property to represent their state which makes them behave like they are "linked"

You can solve this by creating separate definition of input state for each input component:

type InputIds = 'password' | 'reenter'
type State = {[K in InputIds]: PayloadType }
const defaultState: State = {
  password: {
    inputType: 'password',
    isShowPassword: false
  },
  reenter: {
    inputType: 'password',
    isShowPassword: false
  }
}

the reducer function nalso needs to be updated:

const reducerFunction = (state: State, action: ActionTypes): State => {

    switch (action.type) {
        case 'SHOW':
            return {
                ...state,
                [action.payload.target]: {
                    inputType: action.payload.inputType,
                    isShowPassword: action.payload.isShowPassword,
                }
            }
        case 'HIDE':
            return {
                ...state,
                [action.payload.target]: {
                    inputType: action.payload.inputType,
                    isShowPassword: action.payload.isShowPassword,
                }
            }

    }
}

Now in the functional componente I gonna highligh only the important changes. The first one the usage of the new reducer function:

const [ state, dispatch ] = useReducer<Reducer<any, any>>( reducerFunction, defaultState)

the second one is in the actual input components:

<div >
    <input type={inputType} name="password"     placeholder='reenter password'
    />
    {
        state.reenter.isShowPassword === false ? <button onClick={(e)=> {
                e.preventDefault()
                return dispatch!({
                type: 'SHOW',
                payload: {
                    inputType:'text',
                    isShowPassword: true,
                    target: 'reenter'
                }
            })}
        }>
            Show
        </button>
        :<button onClick={ (e)=> {
                e.preventDefault()
                    return dispatch!({
                        type: 'HIDE',
                        payload: {
                            inputType: 'password',
                            isShowPassword: false,
                            target: 'reenter'
                        }
                    })
                }
            } 
            >
            Hide
        </button>
    }
</div>

Please let me know if this was helpful or if a made any mistakes


I also would like to take the opportunity to show a different way to do what you are trying to do.

You can use an array of booleans to represent the visibility (password/text) state of each input and you can also remove some of the duplicate code using more ternary operators.


const Signup = () => {
    const [inputsVisible, setInputsVisible] = useState<[boolean, boolean]>([
        false,
        false,
    ])

    const toggleInputVisibility = (index: 0 | 1) => {
        if (index === 0) {
            setInputsVisible((prev) => [!prev[0], prev[1]])
        } else {
            setInputsVisible((prev) => [prev[0], !prev[1]])
        }
    }

    return (
        <div>
            <input
                type={inputsVisible[0] ? 'text' : 'password'}
                name="password"
                placeholder="create password"
            />
            <button onClick={() => toggleInputVisibility(0)} >
                { inputsVisible[0] ? 'Hide' : 'Show' }
            </button>
            <br />
            <input
                type={inputsVisible[1] ? 'text' : 'password'}
                name="password"
                placeholder="reenter password"
            />
            <button onClick={() => toggleInputVisibility(1)} >
            { inputsVisible[1] ? 'Hide' : 'Show' }
            </button>
        </div>
    )
}

Upvotes: 1

Related Questions