Home > OS >  How to map an array of objects in React useReducer function
How to map an array of objects in React useReducer function

Time:10-02

I have an array of objects in my React state. I want to be able to map through them, find the one I need to update and update its value field. The body of my request being sent to the server should look like: { name: "nameOfInput", value:"theUserSetValue" type: "typeOfInput" }

What I thought would be simple is causing me some heartache. My reducer function calls, and I hit the "I AM RUNNING" log where it then jumps over my map and simply returns my state (which is empty). Please note that I NEVER see the "I SHOULD RETURN SOMETHING BUT I DONT" log.

NOTE: I have learned that I could be simply handingling this with useState

function Form(props) {
    const title = props.title;
    const paragraph = props.paragraph;
    const formBlocks = props.blocks.formBlocks
    const submitEndpoint = props.blocks.submitEndpoint || "";
    const action = props.blocks.action || "POST";

    const formReducer = (state, e) => {
        console.log("I AM RUNNING")
        state.map((obj) => {
            console.log("I SHOULD RETURN SOMETHING BUT I DONT")
            if (obj.name === e.target.name) {
                console.log("OBJ EXISTS", obj)
                return {...obj, [e.target.name]:obj.value}
            } else {
                console.log("NO MATCH", obj)
                return obj
            }
        });
        return state
    }

    const [formData, setFormData] = useReducer(formReducer, []);
    const [isSubmitting, setIsSubmitting] = useState(false);

===================================================================== Where I am calling my reducer from:

<div className="form-block-wrapper">
                {formBlocks.map((block, i) => {
                    return <FormBlock 
                    key={block.title   i}
                    title={block.title}
                    paragraph={block.paragraph}
                    inputs={block.inputs}
                    buttons={block.buttonRow}
                    changeHandler={setFormData}
                    />
                })}
            </div>

CodePudding user response:

Issues

When using the useReducer hook you should dispatch actions to effect changes to the state. The reducer function should handle the different cases. From what I see of the code snippet it's not clear if you even need to use the useReducer hook.

When mapping an array not only do you need to return a value for each iterated element, but you also need to return the new array.

Solution

Using useReducer

const formReducer = (state, action) => {
  switch(action.type) {
    case "UPDATE":
      const { name, value } = action.payload;
      return state.map((obj) => obj.name === name
        ? { ...obj, [name]: value }
        : obj
      );
    
    default:
      return state;
  }
};

...

const [formData, dispatch] = useReducer(formReducer, []);

...

{formBlocks.map((block, i) => {
  return (
    <FormBlock 
      key={block.title   i}
      title={block.title}
      paragraph={block.paragraph}
      inputs={block.inputs}
      buttons={block.buttonRow}
      changeHandler={e => dispatch({
        type: "UPDATE",
        payload: {...e.target}
      })}
    />
  );
})}

Using useState

const [formData, setFormData] = useState([]);

...

const changeHandler = e => {
  const { name, value } = e.target;
  setFormData(data => data.map(obj => obj.name === name
    ? { ...obj, [name]: value }
    : obj
  ));
};

...

{formBlocks.map((block, i) => {
  return (
    <FormBlock 
      key={block.title   i}
      title={block.title}
      paragraph={block.paragraph}
      inputs={block.inputs}
      buttons={block.buttonRow}
      changeHandler={changeHandler}
    />
  );
})}

CodePudding user response:

I have come to understand my problem much better now and I'll update my question to reflect this.

  1. As the user interacted with an input I needed to figure out if they had interacted with it before
  2. If they did interact with it before, I needed to find that interaction in the state[] and update the value as required
  3. If they didn't I needed to add an entirely new object to my forms state[]

I wrote two new functions, an AddObjectToArray function and an UpdateObjectInArray function to serve these purposes.

const handleFormInputChange = (e) => {
    const { name, value, type } = e.target;

    const addObjectToArray = (obj) => {
        console.log("OBJECT TO BE ADDED TO ARRAY:", obj)
        setFormData(currentArray => ([...currentArray, obj]))
    }
    
    const updateObjectInArray = () => {
        const updatedObject = formData.map(obj => {
            if (obj.name === name) {
                //If the name matches, Update the value of the input
                return ({...obj, value:value})
            }
            else {
                //if no match just return the object as is
                return obj
            }
        })
        setFormData(updatedObject)
    } 
    //Check if the user has already interacted with this input
    if (formData.find(input => input.name === name)) {
        updateObjectInArray()
    }
    else {
        addObjectToArray({name, value, type})
    } 
}

I could get more complicated with this now and begin to write custom hooks that take a setState function as a callback and the data to be handled.

  • Related