简体   繁体   中英

Changing boolean value with React Bootstrap checkbox and hooks

Trying to change the isVegan object (nested boolean) with React Bootstrap checkbox and hooks. I can access the object without any issues (eg checkbox is checked if isVegan is true), but have been unable to modify the state. As you can see in the Redux dev tools (image link included), the isVegan object is passed through my state and is accessible. I have also used similar code for the other objects in the chef collection without any issues so believe the issue is either related to the checkbox or how the isVegan object is nested in the chef collection. (Lastly, I know some of the code below may be extra, I slimmed down my original file to simplify this example)

import React, { useState, useEffect, setState } from 'react';
import { Form, Button, Row, Col, Tabs, Tab } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { getChefDetails, updateChefProfile } from '../../actions/chefActions';
import { CHEF_UPDATE_PROFILE_RESET } from '../../constants/chefConstants';
import FormContainer from '../../components/FormContainer/FormContainer.component';

import './ProfileEditPage.styles.scss';

const ProfileEditPage = ({ location, history }) => {
  const [first_name, setFirstName] = useState('')
  const [last_name, setLastName] = useState('')
  const [username, setUsername] = useState('')
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
  const [isVegan, setIsVegan] = useState('')
  const [bio, setBio] = useState('')
  const [message, setMessage] = useState(null)

  const dispatch = useDispatch()

  const chefDetails = useSelector(state => state.chefDetails)
  const { loading, error, chef } = chefDetails

  const chefLogin = useSelector(state => state.chefLogin)
  const { chefInfo } = chefLogin

  const chefUpdateProfile = useSelector(state => state.chefUpdateProfile)
  const { success } = chefUpdateProfile

  useEffect(() => {
    if(!chefInfo) {
      history.push('/login')
    } else {
      if(!chef || !chef.username || success) {
        dispatch({ type: CHEF_UPDATE_PROFILE_RESET })
        dispatch(getChefDetails('profile'))
      } else {
        setFirstName(chef.first_name)
        setLastName(chef.last_name)
        setUsername(chef.username)
        setEmail(chef.email)
        setBio(chef.bio)
        setIsVegan(chef.isVegan)
      }
    }
  }, [dispatch, history, chefInfo, chef, success])

  const submitHandler = (e) => {
    e.preventDefault()
    if (password !== confirmPassword) {
      setMessage('Passwords do not match')
    } else {
      dispatch(updateChefProfile({
        id: chef._id,
        first_name,
        last_name,
        username,
        email,
        password,
        bio,
        isVegan
      }))
    }
  }

  const [key, setKey] = useState('auth')

  //const isVegan = chef.diets[0].isVegan
  //const isVegetarian = chef.diets[0].isVegetarian

  console.log(isVegan)

  return (
    <FormContainer className="profileEditPage">
      <h1>Chef Profile</h1>
      <Form className='profileEditPageForm' onSubmit={submitHandler}>
        <Tabs id="profileEditPageTabs" activeKey={key} onSelect={(k) => setKey(k)}>
          <Tab eventKey='auth' title="Auth">
            <Form.Group controlId='first_name'>
              <Form.Label>First Name</Form.Label>
              <Form.Control
                type='text'
                placeholder='Enter your first name'
                value={first_name}
                onChange={(e) => setFirstName(e.target.value)}
                required
              >
              </Form.Control>
            </Form.Group>

            <Form.Group controlId='last_name'>
              <Form.Label>Last Name</Form.Label>
              <Form.Control
                type='text'
                placeholder='Enter your last name'
                value={last_name}
                onChange={(e) => setLastName(e.target.value)}
                required
              >
              </Form.Control>
            </Form.Group>

            <Form.Group controlId='username'>
              <Form.Label>Username</Form.Label>
              <Form.Control
                type='text'
                placeholder='Enter a username'
                value={username}
                onChange={(e) => setUsername(e.target.value)}
                required
              >
              </Form.Control>
              <Form.Text className='muted'>Your username will be public</Form.Text>
            </Form.Group>

            <Form.Group controlId='email'>
              <Form.Label>Email</Form.Label>
              <Form.Control
                type='email'
                placeholder='Enter your email'
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                required
              >
              </Form.Control>
            </Form.Group>

            <Form.Group controlId='password'>
              <Form.Label>Password</Form.Label>
              <Form.Control
                type='password'
                placeholder='Enter your password'
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              >
              </Form.Control>
            </Form.Group>

            <Form.Group controlId='confirmPassword'>
              <Form.Label>Confirm Password</Form.Label>
              <Form.Control
                type='password'
                placeholder='Confirm password'
                value={confirmPassword}
                onChange={(e) => setConfirmPassword(e.target.value)}
              >
              </Form.Control>
            </Form.Group>
          </Tab>
          <Tab eventKey='chef-detail' title="Chef Detail">
            <Form.Group controlId='isVegan'>
              <Form.Check
                type='checkbox'
                label='Vegan?'
                checked={isVegan}
                value={isVegan}
                onChange={(e) => setIsVegan(e.target.checked)}
              />
            </Form.Group>

            <Form.Group controlId='bio'>
              <Form.Label>Chef Bio</Form.Label>
              <Form.Control
                as='textarea'
                rows='5'
                maxLength='240'
                placeholder='Enter bio'
                value={bio}
                onChange={(e) => setBio(e.target.value)}
              >
              </Form.Control>
              <Form.Text className='muted'>Your bio will be public</Form.Text>
            </Form.Group>
          </Tab>
        </Tabs>

        <Button type='submit' variant='primary'>
          Update
        </Button>
      </Form>


    </FormContainer>

  )
}

export default ProfileEditPage;

Actions

export const getChefDetails = (id) => async (dispatch, getState) => {
  try {
    dispatch({
      type: CHEF_DETAILS_REQUEST
    })

    const { chefLogin: { chefInfo} } = getState()

    const config = {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${chefInfo.token}`
      }
    }

    const { data } = await axios.get(
      `/api/chefs/${id}`,
      config
    )

    dispatch({
      type: CHEF_DETAILS_SUCCESS,
      payload: data
    })

  } catch (error) {
    dispatch({
      type: CHEF_DETAILS_FAILURE,
      payload:
        error.response && error.response.data.message
          ? error.response.data.message
          : error.message,
    })
  }
}

export const updateChefProfile = (chef) => async (dispatch, getState) => {
  try {
    dispatch({
      type: CHEF_UPDATE_PROFILE_REQUEST
    })

    const { chefLogin: { chefInfo } } = getState()

    const config = {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${chefInfo.token}`
      }
    }

    const { data } = await axios.put(
      `/api/chefs/profile`,
      chef,
      config
    )

    dispatch({
      type: CHEF_UPDATE_PROFILE_SUCCESS,
      payload: data
    })

    dispatch({
      type: CHEF_LOGIN_SUCCESS,
      payload: data
    })

    localStorage.setItem('chefInfo', JSON.stringify(data))

  } catch (error) {
    dispatch({
      type: CHEF_UPDATE_PROFILE_FAILURE,
      payload:
        error.response && error.response.data.message
          ? error.response.data.message
          : error.message,
    })
  }
}

Reducers

export const chefDetailsReducer = (state = { chef: { } }, action) => {
  switch(action.type) {
    case CHEF_DETAILS_REQUEST:
      return { ...state, loading: true }
    case CHEF_DETAILS_SUCCESS:
      return { loading: false, chef: action.payload }
    case CHEF_DETAILS_FAILURE:
      return { loading: false, error: action.payload }
    case CHEF_DETAILS_RESET:
      return {
        chef: {}
      }
    default:
      return state
  }
}

export const chefUpdateProfileReducer = (state = { }, action) => {
  switch(action.type) {
    case CHEF_UPDATE_PROFILE_REQUEST:
      return { loading: true }
    case CHEF_UPDATE_PROFILE_SUCCESS:
      return { loading: false, success: true, chefInfo: action.payload }
    case CHEF_UPDATE_PROFILE_FAILURE:
      return { loading: false, error: action.payload }
    case CHEF_UPDATE_PROFILE_RESET:
      return { }
    default:
      return state
  }
}

Controller

// @description Get chef profile
// @route GET /api/chefs/profile
// @access Private
const getChefProfile = asyncHandler(async (req, res) => {
  const chef = await Chef.findById(req.chef._id)

  if(chef) {
    res.json({
      _id: chef._id,
      first_name: chef.first_name,
      last_name: chef.last_name,
      username: chef.username,
      email: chef.email,
      bio: chef.bio,
      isVegan: chef.isVegan
    })
  } else {
    res.status(404)
    throw new Error('Chef not found')
  }
})

// @description Update chef profile
// @route PUT /api/chefs/profile
// @access Private
const updateChefProfile = asyncHandler(async (req, res) => {
  const chef = await Chef.findById(req.chef._id)

  if(chef) {
    chef.first_name = req.body.first_name || chef.first_name
    chef.last_name = req.body.last_name || chef.last_name
    chef.username = req.body.username || chef.username
    chef.email = req.body.email || chef.email
    chef.bio = req.body.bio || chef.bio
    chef.isVegan = req.body.isVegan || chef.isVegan

    if (req.body.password) {
      chef.password = req.body.password
    }

    const updatedChef = await chef.save()

    res.json({
      _id: updatedChef._id,
      first_name: updatedChef.first_name,
      last_name: updatedChef.last_name,
      username: updatedChef.username,
      email: updatedChef.email,
      bio: updatedChef.bio,
      isVegan: updatedChef.isVegan,
      token: generateToken(updatedChef._id),
    })

  } else {
    res.status(404)
    throw new Error('Chef not found')
  }
})

Issue

After much back and forth I believe the issue is with how the response "payload" is stored back in state by the reducer. The response object is a flat object with isVegan at the root, but in state isVegan is in a nested diets array.

res.json({
  _id: updatedChef._id,
  first_name: updatedChef.first_name,
  last_name: updatedChef.last_name,
  username: updatedChef.username,
  email: updatedChef.email,
  bio: updatedChef.bio,
  isVegan: updatedChef.isVegan,
  token: generateToken(updatedChef._id),
})

The reducer takes the payload and also saves it directly to a chefInfo property and overwriting any existing data.

export const chefUpdateProfileReducer = (state = { }, action) => {
  switch(action.type) {
    ...

    case CHEF_UPDATE_PROFILE_SUCCESS:
      return { loading: false, success: true, chefInfo: action.payload }

    ...
  }
}

Solution

Reducer should merge in response payload. In your redux screenshot I don't see a chefInfo key so I'll write this to match the screenshot as closely as possible.

export const chefUpdateProfileReducer = (state = { }, action) => {
  switch(action.type) {
    ...

    case CHEF_UPDATE_PROFILE_SUCCESS:
      const {
        _id,
        isVegan,
        token,
        ...chefDetails // i.e. first & last name, username, email, bio
      } = action.payload;

      return {
        ...state, // <-- shallow copy state
        loading: false,
        success: true,
        chef: {
          ...state.chef, // <-- shallow copy existing chef details
          ...chefDetails, // shallow copy new chef details
          diets: state.chef.diets.map(diet => diet._id === _id ? { // <-- map existing state
            ...diet, // <-- shallow copy diet object
            isVegan // <-- overwrite isVegan property
          } : diet),
        },
      };

    ...
  }
}

Note: This is a best guess to state structures and types since your reducers appear to have a very minimally defined initial state, so this likely needs to be tweaked to fits your exact state structure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM