Search code examples
reactjsreact-hooksuse-effectuse-stateasynccallback

React functional component, useState call back without using useEffect


I'm pretty new to reactJS, and may have either over complicated things or didn't plan it properly.

I have encountered an issue with using the useState hook to keep track of properties in my project.

ok, so here goes

  1. value is taken from an input text box as type text, due to using react-hook-form
  2. type is then checked, to see if it's equal to "Income" or "Expense"
  3. If type == "income" then just set the value using setValue without any changes, else if type == "expense" then *=-1 and set value using setValue
  4. parseInt() is used with the setValue to make sure the value is saved as a number
  5. setRecords is called to add the relevant information into record array of objects.

so as you can see from the 5 steps above, useState hook setValue and setRecords is getting called one after the other, the issue I'm having at the moment is that on the initial run of the app, the value would append instead of adding.

from what I understand is that because the useState hook is asynchronous and does not return a result immediately, so by the time the setRecords is called, value still of type text instead of number

I have been searching online for a while but was unable to find a good way to resolve this issue, without using useEffect hook, I have tried to use useEffect hook, but I didn't manage to get it to work, the user is allowed to enter the same value and the app should still append the value into the record array of objects.

in the end, I decided to create a new property currentValue and set it on form submit, because this is not a state and will update immediately, it won't have to wait for setValue to update the value state.

Is there a better way to work around this issue? or is the workaround ok?

import React, { useEffect, useContext } from "react";
import RecordList from "./RecordList";
import { useForm } from "react-hook-form";
import { BudgetPlannerContext } from "../context/BudgetPlannerContext";

const BudgetPlanner = () => {
  const { register, handleSubmit, errors } = useForm();
  const [
    records,
    setRecords,
    total,
    setTotal,
    type,
    setType,
    value,
    setValue,
    description,
    setDescription,
  ] = useContext(BudgetPlannerContext);

  let currentValue = 0;

  useEffect(() => {
    setTotal(() =>
      records.reduce((acc, curr) => {
        return acc + curr.value;
      }, 0)
    );
  }, [records, setTotal]);

  const onFormSubmit = (data) => {
    if ((type === "Income") & (data.value !== 0)) {
      currentValue = parseInt(data.value);
      setValue(parseInt(data.value));
    } else if ((type === "Expense") & (data.value !== 0)) {
      currentValue = parseInt((data.value *= -1));
      setValue(parseInt((data.value *= -1)));
    } else {
      return null;
    }
    setDescription(data.description);
    processValue(type, currentValue);
  };

  const processValue = (type, currentValue) => {
    updateRecord(type, currentValue);
    //setValue(0);
    //setDescription("");
    //console.log(records);
  };

  const updateRecord = (type, currentValue) => {
    setRecords((oldRecord) => [
      ...oldRecord,
      {
        type: type,
        value: currentValue,
        description: description,
        id: Math.random() * 1000,
      },
    ]);
  };

  return (
    <div className="ui segment">
      <div className="search-bar ui segment">
        <form className="ui form" onSubmit={handleSubmit(onFormSubmit)}>
          <div className="field">
            <select
              onChange={(e) => setType(e.target.value)}
              name="todos"
              ref={register}
              className="filter-todo"
            >
              <option value="" defaultValue="defaultValue">
                ---
              </option>
              <option value="Income">Income</option>
              <option value="Expense">Expense</option>
            </select>
            <input
              name="description"
              type="text"
              placeholder="Description"
              ref={register}
            />
            <input
              name="value"
              type="number"
              placeholder="Value"
              ref={register}
            />
          </div>
          <div>Total: {total}</div>
          <button type="submit">Submit</button>
        </form>
      </div>
      <RecordList records={records} setRecords={setRecords} />
    </div>
  );
};

export default BudgetPlanner;


Solution

  • If I were you, I'd just pass the new value as a parameter to the other functions you're calling:

    const onFormSubmit = (data) => {
        if (data.value === 0 || (type !== 'Income' && type !== 'Expense')) {
            return;
        }
        const newValue = parseInt(data.value) * (type === 'Income' ? 1 : -1);
        setValue(newValue);
        setDescription(data.description);
        processValue(type, newValue);
    };
    

    No need for an extra outer variable.

    Another option is to use useEffect (or, preferably, useLayoutEffect) and call processValue when value changes:

    const onFormSubmit = (data) => {
        if (data.value === 0 || (type !== 'Income' && type !== 'Expense')) {
            return;
        }
        const newValue = parseInt(data.value) * (type === 'Income' ? 1 : -1);
        setValue(newValue);
        setDescription(data.description);
    };
    useLayoutEffect(() => {
        if (value) {
            processValue(type);
        }
    }, [value]);
    

    and have processValue use the outer value stateful variable.