Common toggle function doesn't work well with setState() - reactjs

I have a state which represents this interface:
StateInterface {
variableOne: string;
variableTwo: boolean;
variableThree: boolean;
// ...
}
And toggle function:
toggleFunction = (value: keyof StateInterface): void => {
this.setState((state) => ({
[value]: !state[value]
}));
};
Not all variables are boolean in my state
But TSLint is telling me that the function is missing some properties.
Is it possible to use only one toggle function for my state?

so what about to make your function to only accpet keys that are boolean in your interface?
interface IStateInterface {
variableOne: string;
variableTwo: boolean;
variableThree: boolean;
// ...
}
// here you make type filtered type
type FilterType<Base, Condition> = {
[Key in keyof Base]:
Base[Key] extends Condition ? Key : never
};
// so this is type where all type that don't go throught condition are "never"
type filteredState = FilterType<IStateInterface, boolean>;
and here just pick the keys
type allowedKeys = filteredState [keyof filteredState];
so in your function you have
toggleFunction = (value: allowedKeys ): void => {
this.setState((state) => ({
[value]: !state[value]
}));
};
playground

Could you please more elaborate the query more specifically about what your toggle function will do.
From above toggle function I can see that you are changing the state of a boolean state variable.
What about variableOne which is string.
For your case you have to setState conditional based on the value.
If-else might work to check value is boolean or not.
toggleFunction = (value: keyof StateInterface): void => {
if (typeof variable === "boolean")
{
this.setState((prevState) => ({
[value]: !prevState[value]
}));
} else {
this.setState((prevState) => ({
[value]: 'Some value'
}));
}
};

for setState method we have 2 options for using:
In your case, you used function, so which will be mutated only property not for all state's properties.
You can try to use this code:
toggleFunction = (value: keyof StateInterface): void => {
this.setState((state) => {
value.forEach(property => {
if( typeof(state[property]) != 'string'){
state[property]= !state[property]
}
});
return state;
};

I don't know why, but it works:
this.setState((state) => ({
...state,
[value]: !state[value],
}));
Without ...state I get an error about missing fields

Related

How to use useState and useEffect over array

In the code below, I have a few textareas where the user should input jsons.
I tried to use and array (well, object with numeric keys, just for the ease from rest operator assignment), and I also wanted to avoid parsing both JSONs at every change of either.
It works well, except that for the first render, I get validation only on the second json. I am pretty sure it comes down to scope, with old values being stored in the useEffect, but I added the callback versions of setErrors and that didn't work.
What am I doing wrong, and what is the right pattern for this?
PS: I intend to move each textarea into individual components of course, but would like to understand what is wrong here first.
const [jsons, setJsons] = useState<{ [k: number]: string }>({ 0: '', 1: '' });
const [containers, setContainers] = useState<{ [k: number]: object }>({ 0: {}, 1: {} });
const [errors, setErrors] = useState<{ [k: number]: string }>({ 0: '', 1: '' });
const useContainerEffect = (index: number) => {
useEffect(() => {
let container = {};
try {
container = JSON.parse(jsons[index]);
setErrors(() => ({ ...errors, [index]: '' }))
} catch (e) {
setErrors(() => ({ ...errors, [index]: VALUE_IS_NOT_JSON }))
}
setContainers(() => ({ ...containers, [index]: container }))
}, [jsons[index]]);
}
useContainerEffect(0);
useContainerEffect(1);
const jsonInputChange = (e: HTMLTextAreaElement, i: number) => {
setJsons({ ...jsons, [i]: e.value })
}
Try using functional form of useState to set your errors. Also it's a good practice not to use complex dependencies in dependency array, so they can be statically checked:
const useContainerEffect = (index: number) => {
useEffect(() => {
let container = {};
try {
container = JSON.parse(jsons[index]);
setErrors((err) => ({ ...err, [index]: '' }));
} catch (e) {
setErrors((err) => ({ ...err, [index]: 'VALUE_IS_NOT_JSON' }));
}
setContainers(() => ({ ...containers, [index]: container }));
}, [index]);
};

ContextApi, useReducer, & Typescript - calculated value is not accessible on component

Apologies for the somewhat opaque title, but I am having difficulties being more precise here.
So I have a Context/Reducer Logic, where I initialise the context with some values. I then have a reducer Logic on a custom Provider and use useMemo to calculate values. When trying to access one on of those values (that isn't in the state/initialState) on a component typescript gets angry at me and tells me that said value does not exist on State. What is the best way to remedy this warning?
I have the following definition of a Context/Reducer.
interface State {
displaySidebar: boolean
}
const initialState = {
displaySidebar: false
}
type Action =
| {
type: 'OPEN_SIDEBAR'
}
| {
type: 'CLOSE_SIDEBAR'
}
const UIContext = React.createContext<State>(initialState)
UIContext.displayName = 'UIContext'
const uiReducer = (state: State, action: Action): State => {
switch (action.type) {
case 'OPEN_SIDEBAR': {
return {
...state,
displaySidebar: true,
}
}
case 'CLOSE_SIDEBAR': {
return {
...state,
displaySidebar: false,
}
}
}
}
const UIProvider: FC = (props) => {
const [state, dispatch] = React.useReducer(uiReducer, initialState)
const openSidebar = (): void => dispatch({ type: 'OPEN_SIDEBAR' })
const closeSidebar = (): void => dispatch({ type: 'CLOSE_SIDEBAR' })
const value = useMemo(
() => ({
...state,
openSidebar,
closeSidebar,
}),
[state]
)
return <UIContext.Provider value={value} {...props} />
}
export const useUI = () => {
const context = React.useContext(UIContext)
if (context === undefined) {
throw new Error(`useUI must be used within a UIProvider`)
}
return context
}
export const ManagedUIContext: FC = ({ children }) => (
<UIProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
)
now when I try to use const {closeSidebar} = useUI() in a component typescript gets angry with me and tells me that Property 'closeSidebar' does not exist on type 'State'. I get that, but I was not able to figure out how to properly add closeSidebar to the React.Context type.
When you create context you tell TS that its type will be State, so it doesn't expect anything else to be there. If you want to add additional fields you can create an intersection type, state + methods, either as a named type of just React.createContext<State & {openSidebar : ()=> void, closeSidebar: ()=> void}>. Note that as your initial state doesn't have methods you either need to make them optional or provide some sort of dummy versions.

React useState with Object with multiple boolean fields

I have this object initialised with useState:
const [
emailNotifications,
setEmailNotifications,
] = useState<emailNotifications>({
rating: false,
favourites: false,
payments: false,
refunds: false,
sales: false,
});
And I created a function that should dynamically change the value for each field but I am struggling with assigning the opposite boolean value onClick. This is the function:
const handleEmailNotificationsSettings = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setEmailNotifications({
...emailNotifications,
[event.target.id]: !event.target.id,
});
};
What am I doing wrong?
Your approach is right just one minor thing that you trying to achieve here is wrong.
setEmailNotifications({
...emailNotifications,
[event.target.id]: !event.target.id, //Here
});
when you are setting dynamic value to the state you are expecting it to be the Boolean value which is not
solution:
setEmailNotifications({
...emailNotifications,
[event.target.id]: !emailNotifications[event.target.id],
});
#kunal panchal's answer is totally valid Javascript, but it does cause a Typescript error because the type of event.target.id is string so Typescript does not know for sure that it's a valid key of emailNotifications. You have to assert that it is correct by using as.
!emailNotifications[event.target.id as keyof EmailNotifications]
One way to avoid this is to get the boolean value by looking at the checked property on the input rather than toggling the state.
As a sidenote, it's a good best practice to get the current state by using a setState callback so that you always get the correct value if multiple updates are batched together.
const _handleEmailNotificationsSettings = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setEmailNotifications(prevState => ({
...prevState,
[event.target.id]: event.target.checked,
}));
};
This is probably the best solution for a checkbox.
Another approach which is more flexible to other situations is to use a curried function. Instead of getting the property from the event.target.id, where it will always be string, we pass the property as an argument to create an individual handler for each property.
const handleEmailNotificationsSettings = (
property: keyof EmailNotifications
) => () => {
setEmailNotifications((prevState) => ({
...prevState,
[property]: !emailNotifications[property]
}));
};
or
const handleEmailNotificationsSettings = (
property: keyof EmailNotifications
) => (event: React.ChangeEvent<HTMLInputElement>) => {
setEmailNotifications((prevState) => ({
...prevState,
[property]: event.target.checked
}));
};
which you use like this:
<input
type="checkbox"
checked={emailNotifications.favourites}
onChange={handleEmailNotificationsSettings("favourites")}
/>
Those solutions avoid having to make as assertions in the event handler, but sometimes they are inevitable. I am looping through your state using (Object.keys(emailNotifications) and I need to make an assertion there because Object.keys always returns string[].
import React, { useState } from "react";
// I am defining this separately so that I can use typeof to extract the type
// you don't need to do this if you have the type defined elsewhere
const initialNotifications = {
rating: false,
favourites: false,
payments: false,
refunds: false,
sales: false
};
type EmailNotifications = typeof initialNotifications;
const MyComponent = () => {
// you don't really need to declare the type when you have an initial value
const [emailNotifications, setEmailNotifications] = useState(
initialNotifications
);
const handleEmailNotificationsSettings = (
property: keyof EmailNotifications
) => (event: React.ChangeEvent<HTMLInputElement>) => {
setEmailNotifications((prevState) => ({
...prevState,
[property]: event.target.checked
}));
};
return (
<div>
{(Object.keys(emailNotifications) as Array<keyof EmailNotifications>).map(
(property) => (
<div key={property}>
<label>
<input
type="checkbox"
id={property}
checked={emailNotifications[property]}
onChange={handleEmailNotificationsSettings(property)}
/>
{property}
</label>
</div>
)
)}
</div>
);
};
export default MyComponent;

how i can use Generic type in function body

i'm trying to encapsulate some logic inside component OrderGuard ( which can work with two type of orders: CheckinOrder | Checkout order, but, when i'm try to pass order to orderLoad callback, typescript start complain like so
CheckinOrder is assignable to the constraint of type T but T could be instantiated with a different subtype of constaint 'Order'
type Order = CheckoutOrder | CheckinOrder;
interface Props<T extends Order> {
orderId: string;
orderLoaded: boolean;
onOrderLoad: (order: T) => void;
loadOrder: UseCheckinOrder | UseCheckoutOrder;
children?: React.ReactElement;
}
const isCheckinOrder = (order: Order): order is CheckinOrder => {
return !('servicesFallbackURL' in order);
};
const OrderGuard: <T extends Order>(props: Props<T>) => React.ReactElement<Props<T>> = ({
orderId,
orderLoaded,
onOrderLoad,
loadOrder,
children
}) => {
const [userHasAccess, setUserHasAccess] = useState(true);
const { refetch, loading } = loadOrder(orderId, { skip: true });
const handleOrderLoad = (order: Order) => {
if (isCheckinOrder(order)) {
onOrderLoad(order as CheckinOrder); // <-- error here
} else {
onOrderLoad(order as CheckoutOrder); // <-- and here
}
};
i think i miss something but can't figure out what
I'm pretty new in typescript, how others handle this situations?
component call looks like
<OrderGuard<CheckoutOrder>
orderId={orderId}
orderLoaded={!!order}
onOrderLoad={fillOrder}
loadOrder={useOrder}
>
<Checkout startNewSearch={startNewSearch} />
</OrderGuard>
Not making the Prop interface generic will fix your problem in this instance.
type Order = CheckoutOrder | CheckinOrder;
interface Props {
orderId: string;
orderLoaded: boolean;
onOrderLoad: (order: Order) => void;
loadOrder: UseCheckinOrder | UseCheckoutOrder;
children?: React.ReactElement;
}
const isCheckinOrder = (order: Order): order is CheckinOrder => {
return !('servicesFallbackURL' in order);
};
const OrderGuard: (props: Props) => React.ReactElement<Props> = ({
orderId,
orderLoaded,
onOrderLoad,
loadOrder,
children
}) => {
const [userHasAccess, setUserHasAccess] = useState(true);
const { refetch, loading } = loadOrder(orderId, { skip: true });
const handleOrderLoad = (order: Order) => {
if (isCheckinOrder(order)) {
onOrderLoad(order); // <-- no need to cast
} else {
onOrderLoad(order as CheckoutOrder); // <-- no more errors
}
};

React + Typescript: How to type event.target.name to state?

I have som react state that was defined as an interface and has specificall named keys...
I tried a solution below that should technically work based on the state keys, but it still gives me the error
{ [x: string]: string; }' provides no match for the signature ...
What is the best way to do this...
interface State {
responses: string,
comments: string,
}
state = {
responses: '',
comments: '',
};
handleChange = (e: React.ChangeEvent<HTMLInputElement>, value: string): void => {
const key = e.currentTarget.name;
Object.keys(this.state).forEach(k => {
if (k === key) this.setState({ [e.currentTarget.name]: value });
})
}
The return type of Object.keys() is the generic string[] rather than an array of the union of the keys of the object, so it's probably tricky to infer the correct types here. Moreover, in my experience, smart solutions have a tendency to break when newer versions of TypeScript or package type definitions are released, so in this case I would just help TypeScript with a signature on to the argument of setState:
handleChange = (e: React.ChangeEvent<HTMLInputElement>, value: string): void => {
const key = e.currentTarget.name;
if (Object.keys(this.state).includes(key)) {
this.setState({[key]: value } as Pick<State, keyof State>);
}
}
public onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
[e.currentTarget.name]: e.currentTarget.value
} as { [K in keyof IState]: IState[K] });
};
IState - interface of this.state
One option would be instead of iterating through keys, to use switch statement. Although it will produce more code:
switch (key) {
case 'responses':
this.setState({ responses: value });
break;
case 'comments':
this.setState({ comments: value });
break;
}
If you declare state with type number then use this code for update state:
this.setState({ ...this.state, [event.currentTarget.name]: Number(event.target.value) });
If you declare state with type string then use this code for update state:
this.setState({ ...this.state, [event.currentTarget.name]: event.target.value });
Full code:
onChange =(event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ ...this.state, [event.currentTarget.name]: Number(event.target.value) });
}

Resources