I'm trying to use a redux store state value in an input, but I have a custom useInput hook, that I can't figure how to make them work together.
I built a react custom useInput hook that handles the value, and change/blur events. Used like:
const {
value: titleValue,
error: titleError,
inputChangedHandler: titleChangedHandler,
inputBlurHandler: titleBlurHandler,
setValue: setTitleValue,
} = useInput(validateTitle);
<input
error={titleError ?? false}
id="title"
name="title"
type="text"
placeholder="Stream Title"
autoComplete="off"
value={titleValue}
onChange={titleChangedHandler}
onBlur={titleBlurHandler}
/>
my problem is that if I want to use it in an 'edit' components, which I want to fetch the initial values from an existing state, I cannot do it, because the input value is bound to the useInput value property.
so I can't do this, with my useInput custom hook:
const selectedItem = useSelector((state) => state.items.selectedItem);
<input
error={titleError ?? false}
id="title"
name="title"
type="text"
placeholder="Stream Title"
autoComplete="off"
value={selectedItem.title} <-- use the state selectedItem value
onChange={titleChangedHandler}
onBlur={titleBlurHandler}
/>
my useInput customer hook is just in charge of validation of the input value. It would work well if I could initially set the value to the store value, but my component is using useEffect to call an API getById(id) so the first time the component loads there is still no selectedItem, so I cannot initially set the useInput to the selectedItem.title.
this is my useInput custom hook code:
import { useState } from 'react';
const useInput = (validate) => {
console.log('in useInput');
const [value, setValue] = useState('');
const [isTouched, setIsTouched] = useState(false);
const validationResult = validate(value);
const error = !validationResult.isValid && isTouched && validationResult.message;
const inputChangedHandler = (event) => {
setIsTouched(true);
setValue(event.target.value);
};
const inputBlurHandler = () => {
setIsTouched(true);
};
return { value, error, inputChangedHandler, inputBlurHandler, setValue };
};
export default useInput;
How can I fix it?
Related
Edit: Small changes for readability.
I'm new to react and I may be in at the deep end here but I'll go ahead anyway..
I have a Login component in which I want to give the users feedback when the input elements lose focus and/or when the user clicks submit.
I am aware that I achieve a similar bahavior with useState but for the sake of education I'm trying with useRef.
I'm getting a TypeError for undefined reading of inputRef in LoginForm.js. So inputRef is not assigned a value when validateInput is called. Can anyone help me make sense of why that is and whether there is a solution to it?
LoginForm.js:
import useInput from '../../hooks/use-input';
import Input from '../../UI/Input/Input';
const LoginForm = () => {
const { inputRef, isValid } = useInput(value =>
value.includes('#')
);
return <Input ref={inputRef} />;
};
use-input.js (custom hook):
const useInput = validateInput => {
const inputRef = useRef();
const isValid = validateInput(inputRef.current.value);
return {
inputRef,
isValid,
};
};
Input.js (custom element component):
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props.input}></input>;
});
One issue that I'm seeing is that in the Input component, you're using props.input, why?
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props}></input>;
});
You want exactly the props that you're sending to be assigned to the component.
Next up, you're doing value.includes('#'), but are you sure that value is not undefined?
const { inputRef, isValid } = useInput(value =>
value && value.includes('#')
);
This would eliminate the possibility of that error.
Solving the issue with the inputRef is undefined is not hard to fix.
Afterward, you're going to face another issue. The fact that you're using useRef (uncontrolled) will not cause a rerender, such that, if you update the input content, the isValid won't update its value.
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. (React Docs)
This is a personal note, but I find uncontrolled components in general hard to maintain/scale/..., and also refs are not usually meant to do this kind of stuff. (yes, yes you have react-form-hook which provides a way of creating forms with uncontrolled components, and yes, it's performant).
In the meantime, while I'm looking into this a little more, I can provide you a solution using useState.
const useInput = (validationRule, initialValue='') => {
const [value, setValue] = useState(initialValue)
const onChange = (e) => setValue(e.target.value)
const isValid = validationRule && validationRule(value)
return {
inputProps: {
value,
onChange
},
isValid
}
}
So, right here we're having a function that has 2 parameters, validationRule and initialValue(which is optional and will default to text if nothing is provided).
We're doing the basic value / onChange stuff, and then we're returning those 2 as inputProps. Besides, we're just calling the validationRule (beforehand, we check that it exists and it's sent as parameter).
How to use:
export default function SomeForm() {
const { inputProps, isValid } = useInput((value) => value.includes('#'));
return <Input {...inputProps}/>;
}
The following part is something that I strongly discourage.
This is bad but currently, the only way of seeing it implemented with refs is using an useReducer that would force an update onChange.
Eg:
const useInput = (validationRule) => {
const [, forceUpdate] = useReducer((p) => !p, true);
const inputRef = useRef();
const onChange = () => forceUpdate();
const isValid = validationRule && validationRule(inputRef.current?.value);
return {
inputRef,
isValid,
onChange
};
};
Then, used as:
export default function SomeForm() {
const { inputRef, onChange, isValid } = useInput((value) =>
value && value.includes("#")
);
console.log(isValid);
return <Input ref={inputRef} onChange={onChange} />;
}
I have the following input:
<input
name="name"
type="text"
data-testid="input"
onChange={(e) => setTypedName(e.target.value)}
value=""
/>
the test:
test.only('Should change the value of the input ', async () => {
makeSut()
const nameInput = sut.getByTestId('input') as HTMLInputElement
fireEvent.change(nameInput, { target: { value: 'value' } })
expect(nameInput.value).toBe('value')
})
My assertion fails, as the change does not take effect, while the value remains to be ""
If I remove value="" from the input, change takes effect.
I have tried using fireEvent.input fireEvent.change, userEvent.type and nothing works.
It seems that when I use a default value the testing library does not accept changes, even though it works on production...
Any hints?
Using:
jest 27.3.1
#testing-library/react 12.1.2
#testing-library/user-event 13.5.0
I'm not sure, but perhaps this is due to React relying more on explicitly setting the value of components through JS rather than through "vanilla" HTML.
Explicitly setting the input value through JS makes your test pass:
import { render, screen } from "#testing-library/react";
import React, { useState } from "react";
import userEvent from "#testing-library/user-event";
const Component = () => {
const [value, setValue] = useState("");
return (
<input
name="name"
type="text"
data-testid="input"
onChange={(e) => setValue(e.target.value)}
value={value}
/>
);
};
test.only("Should change the value of the input ", async () => {
render(<Component />);
const nameInput = screen.getByTestId("input") as HTMLInputElement;
userEvent.type(nameInput, "value");
expect(nameInput.value).toBe("value");
});
PS I slightly modified your test to use render because I'm not sure what makeSut is, and I assume it's some custom function that you created to render your component.
I am quite new to React and how to use hooks. I am aware that the following code doesn't work, but I wrote it to display what I would like to achieve. Basically I want to use useQuery after something changed in an input box, which is not allowed (to use a hook in a hook or event).
So how do I correctly implement this use case with react hooks? I want to load data from GraphQL when the user gives an input.
import React, { useState, useQuery } from "react";
import { myGraphQLQuery } from "../../api/query/myGraphQLQuery";
// functional component
const HooksForm = props => {
// create state property 'name' and initialize it
const [name, setName] = useState("Peanut");
const handleNameChange = e => {
const [loading, error, data] = useQuery(myGraphQLQuery)
};
return (
<div>
<form>
<label>
Name:
<input
type="text"
name="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</div>
);
};
export default HooksForm;
You have to use useLazyQuery (https://www.apollographql.com/docs/react/api/react-hooks/#uselazyquery) if you wan't to control when the request gets fired, like so:
import React, { useState } from "react";
import { useLazyQuery } from "#apollo/client";
import { myGraphQLQuery } from "../../api/query/myGraphQLQuery";
// functional component
const HooksForm = props => {
// create state property 'name' and initialize it
const [name, setName] = useState("Peanut");
const [doRequest, { called, loading, data }] = useLazyQuery(myGraphQLQuery)
const handleNameChange = e => {
setName(e.target.value);
doRequest();
};
return (
<div>
<form>
<label>
Name:
<input
type="text"
name="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</div>
);
};
export default HooksForm;
I think you could call the function inside the useEffect hook, whenever the name changes. You could debounce it so it doesn't get executed at every letter typing, but something like this should work:
handleNameChange = (e) => setName(e.target.value);
useEffect(() => {
const ... = useQuery(...);
}, [name])
So whenever the name changes, you want to fire the query? I think you want useEffect.
const handleNameChange = e => setName(e.target.value);
useEffect(() => {
// I'm assuming you'll also want to pass name as a variable here somehow
const [loading, error, data] = useQuery(myGraphQLQuery);
}, [name]);
I've created a custom hook to use with a form I've built using semantic-ui-react. The code for the hook looks like this (taken from here )
import React, { useState } from 'react'
const useForm = callback => {
const [inputs, setInputs] = useState({})
const handleSubmit = event => {
if (event) {
event.preventDefault()
}
}
const handleInputChange = event => {
event.persist()
setInputs(inputs => ({
...inputs,
[event.target.name]: event.target.value,
}))
}
return {
handleSubmit,
handleInputChange,
inputs,
}
}
export default useForm
It works perfectly well with all of my text based inputs at the moment, but I've added some radio buttons like this:
<Form.Group inline>
<label>Number of Hours</label>
<Form.Radio
label="<3 Hours"
name="hours"
onChange={handleInputChange}
value="1"
checked={inputs.hours === 1}
/>
<Form.Radio
label="3+ Hours"
name="hours"
onChange={handleInputChange}
value="2"
checked={inputs.hours === 2}
/>
</Form.Group>
But the hook doesn't work properly. I've done some digging and it looks like it's because the onChange (I've tried onClick too) seems to fire on the label in semantic-ui-react, so the event doesn't contain the proper target name or value. The only workaround I can think of is to write some custom handler that creates a fake event that looks for the hidden radio input :before the label, but it seems like there should be a cleaner way.
Updated Workaround
I created a custom handler for radios as a temporary workaround and also adjusted the checked part to put quotes around the value. It works, but if anyone knows a better way, please share.
This is the custom handler.
const radioHandleInputChange = e => {
let { value, name } = e.target.previousSibling
e.target.name = name
e.target.value = value
handleInputChange(e)
}
I am playing around with the new React-Redux Hooks library
I have an react component that has two input fields that update to the react store using useState() - desc and amount. In order to update changes to the the redux store when field has been edited I use onBlur event and call dispatch to the redux store. That works fine.
When I want to clear the fields from another component I would like this to work in same manner as for class based functions via connect & map State to Props, however to to this with functional component I need to utilise useSelector(). I cannot do this as the identifiers desc and amount are already used by useState()
What am I missing here?
import { useDispatch, useSelector } from "react-redux"
import { defineItem, clearItem } from "../store/actions"
const ItemDef = props => {
const dispatch = useDispatch()
const [desc, setDesc] = useState(itemDef.desc)
const [amount, setAmount] = useState(itemDef.amount)
//MAPSTATETOPROPS
//I WANT TO HAVE THESE VALUES UPDATED WHEN REDUX STORE CHANGES FROM ANOTHER COMPONENT
//THESE LINES WILL CAUSE ERROR to effect - identifier has already been declared
const desc = useSelector(state => state.pendingItem.desc)
const amount = useSelector(state => state.pendingItem.amount)
return (
<div>
<p>Define new items to be added below - before clicking Add Item</p>
<input
value={desc}
type="text"
name="desc"
placeholder="Description of Item"
onChange={e => setDesc(e.target.value)}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(desc, amount))}
/>
<input
value={amount}
type="number"
name="amount"
placeholder="Amount"
onChange={e => setAmount(e.target.value)}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => {
dispatch(defineItem(desc, amount))
}}
/>
</div>
)
}
export default ItemDef
SOLUTION - WITH FULL CODE IN REPOSITORY
I worked out a solution by using useSelector (to map pendingItem part of redux state to itemDef) and the setEffect hook to apply useState to either state item (from input) or itemDef (from Redux State - this happens when redux is updated by another component or through the ADD ITEM TO INPUT button)
I have posted the working component below. I have also posted this small application to demonstrate how to use reacdt-redux libraries with both class based components and fuinctional components using hooks
The repository is https://github.com/Intelliflex/hiresystem
//**************************************************************************************************
//***** ITEMDEF COMPONENT - Allow entry of new Items (dispatched from button in HireList Table) ****
//**************************************************************************************************
import React, { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { defineItem, clearItem } from '../store/actions'
import _ from 'lodash'
const ItemDef = props => {
//BRING IN DISPATCH FROM REDUX STORE
const dispatch = useDispatch()
//DEFINE SELECTOR - EQUIV TO MAPSTATETOPROPS
const { itemDef } = useSelector(state => ({
itemDef: state.pendingItem
}))
const [item, setItem] = useState({ desc: '', amount: 0 })
const onChange = e => {
setItem({
...item,
[e.target.name]: e.target.value
})
}
const prevItem = useRef(item)
useEffect(() => {
//WE NEED TO CONDITIONALLY UPDATE BASED ON EITHER STORE BEING CHANGED DIRECTLY OR INPUT FORM CHANGING
if (!_.isEqual(item, prevItem.current)) {
//INPUT HAS CHANGED
setItem(item)
} else if (!_.isEqual(item, itemDef)) {
//REDUX STATE HAS CHANGED
setItem(itemDef)
}
prevItem.current = item
}, [item, itemDef]) //Note: item and ItemDef are passed in as second argument in order to use setItem
const clearIt = e => {
dispatch(clearItem())
}
const addIt = e => {
dispatch(defineItem({ desc: 'MY NEW ITEM', amount: 222 }))
}
return (
<div>
<p>Define new items to be added below - before clicking Add Item</p>
<input
value={item.desc}
type='text'
name='desc'
placeholder='Description of Item'
onChange={onChange}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(item))}
/>
<input
value={item.amount}
type='number'
name='amount'
placeholder='Amount'
onChange={onChange}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(item))}
/>
<button onClick={clearIt}>CLEAR ITEM</button>
<button onClick={addIt}>ADD ITEM TO INPUT</button>
</div>
)
}
export default ItemDef