Home > Mobile >  function that behaves the same as setState function, that calls setState
function that behaves the same as setState function, that calls setState

Time:07-18

Lets say I want to update my state from my hook

I would use

setStateHook(myvar)

Now If I want to customise it a bit more, I can use this

setStateHook((prevState) => { return {...prevState, myValue}}

Now the problem is that I want to customise and automate a piece of setInputFields function in my hook, but still call it the same as normal setState

My hook

export const useForm = <T extends string | number | symbol>(
  initialInputData: Record<T, IDynamicFormInput>,
  locale?: string
) => {
//...
  const [inputFields, setInputFields] = useState<Record<T, IDynamicFormInput>>(
    () => getInitialInputFields(initialInputData)
  );

//AutomateSetInputFields  
  const setUpdateInputFields = (
    value: SetStateAction<Record<T, IDynamicFormInput>>,
    autoUpdate?: boolean
  ): void => {
    if (!autoUpdate) {
      setInputFields(value);
    } else {
      //What now? value is either S or (value: S) => void
      //Argument of type '(prevState: Record<T, IDynamicFormInput>) => {}' is not assignable to parameter of type 'SetStateAction<Record<T, IDynamicFormInput>>'.
          //I need to put the modified value inside setInputFields
      setInputFields( ( prevState ) => {
        //Everything has wrong type below
        const newInputFields = { ...value }; 
        Object.keys(newInputFields).forEach((key) => {
         //Do things here
          //newinputFields[key].c = "test"
         })
        return newInputFields;
      });
    }
  };


//Return data so I can use it in my components
  return {
    inputFields,
    setInputFields: setUpdateInputFields,
    resetFields,
    setInputField,
  } as const;

//...

Usage:

setUpdateInputFields(myVar, true) //Should work if normal object
setUpdateInputFields((prevState) => { return {...prevState}}, true) //Breaks since I don't know how to process a function

//example input and output:

const obj = {
  a: {
   aa: 1
   ab: 2
  }
  b: {
   ba: 2
   bb: 3
  }
}

setUpdateInputFields((prevState) => { return {...prevState, obj}}, true) 

//Output
{
  a: {
   num1: 1
   mum2: 2
   res: 3 //added
  }
  b: {
   num1: 2
   num2: 3
   res: 5 //added
  }
}

How can I use my function as same as useState, modify the incoming data, and call setState?

CodePudding user response:

Here I do not see why you need prevState when you do not even use it.

setInputFields( ( prevState ) => {
    Object.keys(newInputFields).forEach((key) => {
       return newInputFields;
    });
}

prevState not used.

You also need to return this

return Object.keys(newInputFields) ...

The way you want to use the value as function is by checking the value type.

setInputFields( ( prevState ) => {
    // ... some stuff
    // then do something with the value like this:
    typeof value === 'function' ? value(prevState) : value
}

CodePudding user response:

The value-based update setWhatever(value) is the same as the functional update setWhatever(() => value) (assuming value doesn't change while waiting for the callback), so you could update your function to accept either a function or a non-function and either:

  • Wrap a non-function in a function, so you always know later in the code it's a function

    or

  • Branch where you use it, calling it if it's a function or using it directly if not.

For me, the first option is simpler. The types are a bit of a pain, but not too bad. Here's a basic example (since the code in the question isn't complete, and I wanted to be sure to provide working, runnable example code, I've just done up a fresh example):

function useMyHook<T>(initializer: T): [T, Dispatch<SetStateAction<T>>] {
    const [someState, setSomeState] = useState(initializer);
    const setterRef = useRef<Dispatch<SetStateAction<T>>>();
    if (!setterRef.current) {
        setterRef.current = (update: SetStateAction<T>) => {
            // Get the update we'll use, wrapping a non-function value if
            // necessary
            const updater =
                typeof update === "function"
                    ? (update as Exclude<SetStateAction<T>, T>)
                    : () => update;
            // Always use the functional version of our state setter
            setSomeState((prevValue) => {
                console.log(`Old value is ${JSON.stringify(prevValue)}`);
                const updatedValue = updater(prevValue);
                console.log(`Updated value is ${JSON.stringify(prevValue)}`);
                return updatedValue;
            });
        };
    }

    return [someState, setterRef.current];
}

The only real type trick there is excluding T from SetStateAction<T> because apparently typeof update === "function" wasn't sufficient for TypeScript to understand that on its own. I'm not a fan of type assertions, but that one is guaranteed by the typeof that's part of the same expression.

Live Copy (with TypeScript commented out):

const { useState, useRef } = React;

function useMyHook/*<T>*/(initializer/*: T*/)/*: [T, Dispatch<SetStateAction<T>>]*/ {
    const [someState, setSomeState] = useState(initializer);
    const setterRef = useRef/*<Dispatch<SetStateAction<T>>>*/();
    if (!setterRef.current) {
        setterRef.current = (update/*: SetStateAction<T>*/) => {
            const updater =
                typeof update === "function"
                    ? (update /*as Exclude<SetStateAction<T>, T>*/)
                    : () => update;
            setSomeState((prevValue) => {
                console.log(`Old value is ${JSON.stringify(prevValue)}`);
                const updatedValue = updater(prevValue);
                console.log(`Updated value is ${JSON.stringify(prevValue)}`);
                return updatedValue;
            });
        };
    }

    return [someState, setterRef.current];
}

function Example() {
    const [value, setValue] = useMyHook(0);

    const increment = () => {
        console.log("incrementing");
        setValue((v) => v   1);
    };

    const setToSpecificValue = (value: number) => {
        console.log(`setting to ${value}`);
        setValue(value);
    };

    return (
        <div>
            <div>Value: {value}</div>
            <div>
                <input type="button" value=" " onClick={increment} />
                <input type="button" value="42" onClick={() => setToSpecificValue(42)} />
            </div>
        </div>
    );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>


Note: You may have seen in the code that I store the setter in a ref and never use someState within the setter. That's so that useMyHook provides the same guarantee that useState does: the state setter is stable (doesn't change from one call to the next). From the useState docs:

Note

React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

Your code isn't doing that with setUpdateInputFields, but I recommend doing so. That stability guarantee is very useful with state setters.

  • Related