Search code examples
javascriptreactjsreact-hooksuse-reducer

How to update state from two different handlers using useReducer hooks from React


I'm using { useReducer } to manage my form's state. but I have two separate handlers

  1. handleChange() = for inputs changes (this one works as expected)

const handleChange = async (e) => {
    dispatch({field: e.target.name, value: e.target.value});
  }

  1. UploadFile() = for uploading images (this one doesn't update the images/state)

const uploadFile = async (e) => {
    console.log('Uploading file...');
    const files = e.target.files;
    const data = new FormData();
    data.append('file', files[0]);
    data.append('upload_preset', 'artemis');
    const res = await fetch(`https://api.cloudinary.com/v1_1/${cludinaryAccount}/image/upload`, {
      method: 'POST',
      body: data
    });
    const file = await res.json();
    console.log(file);
    dispatch({
      image: file.secure_url,
      largeImage: file.eager[0].secure_url,
    })
  }

I couldn't update the state of images from with UploadFile(), not sure what I'm doing wrong. Below is the entire complete code.

import React, { useState, useReducer } from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';
import Router from 'next/router';
import Form from './styles/Form';
import formatMoney from '../lib/formatMoney';
import Error from './ErrorMessage';

const CREATE_LISTING_MUTATION = gql`
  mutation CREATE_LISTING_MUTATION(
    $title: String,
    $description: String,
    $address: String,
    $availableFor: String,
    $spaceType: String,
    $height: String,
    $accessType: String,
    $security: String,
    $features: String,
    $nearbyStablishments: String,
    $rules: String,
    $image: String,
    $largeImage: String,
    $price: Int,
    $bond: Int,
    $minBookingStay: Int,
    $size: String,
  ) {
    createListing(
      title: $title
      description: $description
      address: $address
      availableFor: $availableFor
      spaceType: $spaceType
      height: $height
      accessType: $accessType
      security: $security
      features: $features
      nearbyStablishments: $nearbyStablishments
      rules: $rules
      image: $image
      largeImage: $largeImage
      price: $price      
      bond: $bond      
      minBookingStay: $minBookingStay      
      size: $size 
    ) {
      id
    }
  }
`;

const initialState = {
  title: '',
  description: '',
  address: '',
  availableFor: '',
  spaceType: '',
  height: '',
  accessType: '',
  security: '',
  features: '',
  nearbyStablishments: '',
  rules: '',
  image: '',
  largeImage: '',
  price: 0,
  bond: 0,
  minBookingStay: 0,
  size: ''
};

function reducer(state, {field, value}) {
  return {
    ...state,
    [field]: value
  }
}

export const CreateListing = () => {

  const [state, dispatch] = useReducer(reducer,initialState);

  const handleChange = async (e) => {
    dispatch({field: e.target.name, value: e.target.value});
  }
  const { 
    title,
    description,
    address,
    availableFor,
    spaceType,
    height,
    accessType,
    security,
    features,
    nearbyStablishments,
    rules,
    image,
    largeImage,
    price,
    bond,
    minBookingStay,
    size
  } = state;
  
  const uploadFile = async (e) => {
    console.log('Uploading file...');
    const files = e.target.files;
    const data = new FormData();
    data.append('file', files[0]);
    data.append('upload_preset', 'artemis');
    const res = await fetch(`https://api.cloudinary.com/v1_1/${cludinaryAccount}/image/upload`, {
      method: 'POST',
      body: data
    });
    const file = await res.json();
    console.log(file);
    dispatch({
      image: file.secure_url,
      largeImage: file.eager[0].secure_url,
    })
  }
  
  return (
    <Mutation mutation={CREATE_LISTING_MUTATION} variables={state}>
      {
        /* 
          1. Expose createListing function
          2. Expose the error and loading state 
        */
      }
      {(createListing, {error, loading, called}) => {
        // Possible params: error, loading, called, data, info
        return (
          <Form   
            data-test="form"
            onSubmit={ async (e) => {
            // Stop the form from submitting 
            e.preventDefault();
            // Call the mutation
            const res = await createListing();
            // Change them to the single Listing page
            console.log(res);
            Router.push({
              pathname: '/listing',
              query: {
                id: res.data.createListing.id
              }
            });
          }}>  
            <h2>Lease my Space</h2>
            <Error error={error}/>
            {/* area-busy attribute is needed for our loading animation*/ }
            <fieldset disabled={loading} aria-busy={loading}>
            <label htmlFor="file">
                Image
                <input 
                type="file"
                id = "file"
                name = "file"
                placeholder = "Upload an image"
                required
                onChange={uploadFile}
                />
                {image && <img src={image} alt="Image Preview"/> }
              </label>
              <label htmlFor="title">
                Title
                <input 
                type="text"
                id = "title"
                name = "title"
                placeholder = "title"
                required 
                value = {title}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="description">
              Description
                <input 
                type="text"
                id = "description"
                name = "description"
                placeholder = "Description"
                required 
                value = {description}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="address">
              Address
                <input 
                type="text"
                id = "address"
                name = "address"
                placeholder = "address"
                required 
                value = {address}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="availableFor">
              Available For
                <input 
                type="text"
                id = "availableFor"
                name = "availableFor"
                placeholder = "Available For"
                required 
                value = {availableFor}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="spaceType">
              Space Type
                <input 
                type="text"
                id = "spaceType"
                name = "spaceType"
                placeholder = "Space Type"
                required 
                value = {spaceType}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="height">
              Height
                <input 
                type="text"
                id = "height"
                name = "height"
                placeholder = "Height"
                required 
                value = {height}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="accessType">
              Access Type
                <input 
                type="text"
                id = "accessType"
                name = "accessType"
                placeholder = "Access Type"
                required 
                value = {accessType}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="security">
              Security
                <input 
                type="text"
                id = "security"
                name = "security"
                placeholder = "Security"
                required 
                value = {security}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="features">
              Features
                <input 
                type="text"
                id = "features"
                name = "features"
                placeholder = "Features"
                required 
                value = {features}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="nearbyStablishments">
              Nearby Stablishments
                <input 
                type="text"
                id = "nearbyStablishments"
                name = "nearbyStablishments"
                placeholder = "Nearby Stablishments"
                required 
                value = {nearbyStablishments}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="rules">
              Rules
                <input 
                type="text"
                id = "rules"
                name = "rules"
                placeholder = "Rules"
                required 
                value = {rules}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="image">
              Image
                <input 
                type="text"
                id = "image"
                name = "image"
                placeholder = "Image"
                required 
                value = {image}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="largeImage">
              Large Image
                <input 
                type="text"
                id = "largeImage"
                name = "largeImage"
                placeholder = "Large Image"
                required 
                value = {largeImage}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="price">
              Price
                <input 
                type="number"
                id = "price"
                name = "price"
                placeholder = "Price"
                required 
                value = {price}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="bond">
              Bond
                <input 
                type="number"
                id = "bond"
                name = "bond"
                placeholder = "Bond"
                required 
                value = {bond}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="minBookingStay">
              Min Booking stay
                <input 
                type="number"
                id = "minBookingStay"
                name = "minBookingStay"
                placeholder = "size"
                required 
                value = {minBookingStay}
                onChange={handleChange}
                />
              </label>
              <label htmlFor="size">
              size
                <input 
                type="text"
                id = "size"
                name = "size"
                placeholder = "size"
                required 
                value = {size}
                onChange={handleChange}
                />
              </label>
              <button type="submit"> Submit</button>
            </fieldset>
          </Form>
        )
      }}
      </Mutation>
  )
}

export default CreateListing;
export {CREATE_LISTING_MUTATION};


Solution

  • You are not using the useReducer API correctly. You use reducers with actions to determine how the state should be updated based off of the dispatched action. A basic example can be seen in the useReducer docs.

    The reason your reducer isn't working is because it will only every work with an object that has a field and a value property:

    function reducer(state, {field, value}) {
      //                    ^^^^^^^^^^^^^^ needs to be object with field and value props
      return {
        ...state,
        [field]: value
      }
    }
    

    Your handleChange function dispatches an object with those properties. uploadFile dispatches an object without these two properties. In fact, your current reducer will only ever be able to update one key/value pair at a time. A quick fix would be to change the dispatch in uploadFile to be:

    dispatch({ field: 'image', value: file.secure_url });
    dispatch({ field: 'largeImage', value: file.eager[0].secure_url });
    

    That will tell your reducer to update those fields with those values. But this is not the proper usage of useReducer.

    It looks like what you are doing can just be converted to use useState, since as I understand from your reducer function, you are just trying to merge two objects into a new state object. The React docs recommend splitting state out into seperate pieces that often change together, but for simplicity's sake, we'll just stick with one large object.

    In CreateListing we want to use useState:

    const [state, setState] = useState(initialState);
    

    Then in handleChange:

    const handleChange = (e) => {
      setState((oldState) => ({ ...oldState, [e.target.name]: e.target.value }));
    };
    

    And likewise in uploadFile:

    setState((oldState) => ({
      ...oldState,
      image: file.secure_url,
      largeImage: file.eager[0].secure_url,
    }));