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 theuseEffect
oruseCallback
dependency list.
Your code isn't doing that with setUpdateInputFields
, but I recommend doing so. That stability guarantee is very useful with state setters.