How to use useState and useEffect over array - reactjs

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]);
};

Related

setting up custom error validation in react

I am trying to create some custom error validation in React
I have a values obj in state and an error obj in state that share the same keys
const [values, setValues] = useState({
name: "",
age: "",
city: ""
});
const [err, setErr] = useState({
name: "",
age: "",
city: ""
});
i have a very simple handle change and an onSubmit which i want to run my custom validator function inside
const handleChange = (e) => {
setValues({
...values,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
validateForms();
};
in my validateForms function my theory is since both my pieces of state share the same keys I am trying to see if any of those values === '' if yes match is the same key in the err obj and set that respective value to the error and then do other stuff in JSX
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr({
...err,
value: `${value} is a required field`
});
}
}
};
I definitely feel like I'm not using setErr properly here. Any help would be lovely.
link to sandbox: https://codesandbox.io/s/trusting-bartik-6cbdb?file=/src/App.js:467-680
You have two issues. First, your error object key needs to be [value] rather than the string value. Second, you're going to want to use a callback function in your state setter so that you're not spreading an old version of the error object:
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr(err => ({
...err,
[value]: `${value} is a required field`
}));
}
}
};
A more intuitive way to set errors might be to accumulate them all and just set the error state once:
const validateForms = () => {
const errors = {};
for (const value in values) {
errors[value] = values[value] === "" ? `${value} is a required field` : "";
}
setErr(errors);
};

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.

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

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

simulate change not working with trim() enzyme

I was using this test when I had a bug, so I used the trim function for resolve it, and the these test fail, tried in different ways but didn't found the solution
const generalWrapper = shallow(<AddVehiclesTable {...generalProps} />)
const generalInstance = generalWrapper.instance()
describe('onSearchChange', () => {
test('should change the "search" state', () => {
const theFilterValue = 'a new filter value'
generalWrapper.find('.filter-input').simulate('change', { target: { value: theFilterValue } })
const expectedState = Object.assign({}, generalInstance.state)
expectedState.searchValue = { 'target': { 'value': theFilterValue } }
expect(generalInstance.state).toEqual(expectedState)
expect(generalInstance.state.userInteractedWithComponent).toBe(true)
})
})
onSearchChange (searchValue) {
const value = searchValue.trim()
this.setState({ searchValue: value, userInteractedWithComponent: true })
}
Error message
TypeError: searchValue.trim is not a function
Any suggestions
Your function gets the Object as a parameter.
Expose field that you needed
I don't see the whole picture, but can guess that you need something like
onSearchChange ({ target: { value: incomeValue } }) {
const value = incomeValue.trim()
this.setState({ searchValue: value, userInteractedWithComponent: true })
}

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