Enzyme wrapper.update() causes ref input to no longer have value prop - javascript

Here is a Code Sandbox that contains a test simulating this issue. The test in this Code Sandbox fails as described in this question: https://codesandbox.io/s/react-jest-and-enzyme-testing-c7vng
I'm trying to test the value of an <input /> that gets updated inside a useEffect. This is the code from the Code Sandbox, which is a simplified version of something I'm trying to do in a project.
import React, { useEffect, useRef, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
ref.current.value = "";
console.log(typeof ref.current.value);
}, [count]);
const ref = useRef(null);
const handleClick = () => {
setCount(count + 1);
console.log(count);
};
return (
<div>
<input ref={ref} type="text" />
<button onClick={handleClick}>click me</button>
</div>
);
};
export default App;
I use useRef to set the value of the <input />.
The useEffect gets called when the <button /> is clicked. The <button /> updates the useState count. useEffect is watching updates to count, and it gets called as a side-effect.
In the useEffect, I set ref.current.value to an empty string, and then I log the typeof this value to verify that it's a string.
In the test, I try to simulate this behavior:
describe("App", () => {
const wrapper = mount(<App />);
wrapper.find("input").props().value = "hello";
act(() =>
wrapper
.find("button")
.props()
.onClick()
);
console.log(wrapper.find("input").debug());
wrapper.update();
console.log(wrapper.find("input").debug());
expect(wrapper.find("input").length).toBe(1);
expect(wrapper.find("input").props().value).toBe("hello");
});
I set the value prop to 'hello'. I then invoke the <button /> onClick prop, effectively clicking it. I then call wrapper.update(), and I also debug() log the <input /> before and after update().
Before, update(), the <input /> has a value prop containing 'hello'. After the update(), the <input /> does not have a value prop. This causes the test to fail, saying that the <input /> value is undefined after the update.
Shouldn't the input's value be '' after the update?

Here is a list of issues with the current way:
<input ref={ref} type="text" /> is describes a React Element and has no value prop
Prop value should be controlled via state and not mutated directly
wrapper.find("input").props().value = "hello";
Setting value on a DOM Node isn't the same as setting a prop value. Using React means that you ceed DOM manipulation to it.
useRef allows for access to the underlying DOM Node when passed an initial value of null and this following line mutates DOM in spite of App state.
ref.current.value = "";
In certain scenarios, it's expedient to manipulate DOM in spite of the App state. The tests should then deal with the DOM Node and check changes to it.
describe("App", () => {
const wrapper = mount(<App />);
wrapper.find("input").getDOMNode().value = "hello";
act(() =>
wrapper
.find("button")
.props()
.onClick()
);
wrapper.update();
expect(wrapper.find("input").length).toBe(1);
expect(wrapper.find("input").getDOMNode().value).toBe("");
});
If you consider that your use case doesn't require this much of a direct control of the DOMNode.
The input element value prop can be controlled with state. For example,
const App = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState("hello");
useEffect(() => {
setValue("");
}, [count]);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<input type="text" value={value} />
<button onClick={handleClick}>click me</button>
</div>
);
};
export default App;

Related

Adding data to array using UseState onChange

I am having some issues figuring out how I can get the state of an inputfield, and add it to an useState array.
The way this code is set up, using onChange, it will add every character I type in the textField as a new part of the array, but I dont want to set the value until the user is done typing.
What would be a simple solution to this?
My code:
const [subject, setSubject] = useState([]);`
<input type="text" placeholder={"Eks. 'some example'"} onChange={(e) => setSubject(oldArray => [...oldArray, e.target.value])}/>
Well, I am not confident with react yet, but unless you don't want to do some validation, why don't you use useRef hook and onBlur combination. UseRef hook basically set a reference on element and then you can use value from that reference which in your case would be textField value. OnBlur will trigger when user clicks outside of input (input lose focus) Code should look like this:
import react, {useRef, useState} from "react";
const someComponent = (props) => {
const [subject, setSubject] = useState([]);
const textAreaRef = useRef();
const onBlurHandler = () => {
setSubject((prevSubject) => [...prevSubject, textAreaRef.current.value]);
}
return <input type="text" placeholder={"Eks. 'some example'"} ref={textAreaRef} onBlur={onBlurHandler}/>
}
Other way would be to use debouncing with useEffet.
this is a little something i cooked up for you... it watches the change of the input, and 1 second after the person stops typing, it will add the input value.
The main things to look at here are the useEffect() and the <input /> with the new state i made [input, setInput]. or you can play around with this here
export default function App() {
const [subjects,setSubjects] = useState([]);
const [input,setInput] = useState("")
useEffect(() => {
const timer = setTimeout(() => {
setSubjects(old => [...old, input])
}, 1000)
return () => clearTimeout(timer)
}, [input])
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<input placeholder="type here"
value={input}
type="text"
onChange={e => setInput(e.target.value)}
/>
{subjects.length === 0 ?
<h3>Nothing yet...</h3>
:
<h3>{subjects}</h3>
}
</div>
);
}

Avoid rerendering when doing button.click() inside useeffect()

I'm trying to set the data from child component to parent component's state in turn that data will be assigned to input text field. And then I want to automatically click the button to submit the data from input text field to handleBtnSubmit() function.
But when I did this child component is rendering indefinitely. Can someone please help resolving this kind of situation?
Parent
const [responses, setResponses] = useState([]);
const [currentMessage, setCurrentMessage] = useState('');
const submitAction = () => {
setResponses((responses) => [...responses, message]);
handleMessageSubmit(message.text);
setCurrentMessage('');
};
const handleBtnSubmit = () => {
submitAction();
};
<Messages
messages={responses}
parentData={{ currentMessage, setCurrentMessage, btnRef, handleBtnSubmit }}
/>
<div className='typing-area'>
<div className='input-field'>
<input
type='text'
placeholder='Type something here'
required
value={currentMessage}
onChange={handleMessageChange}
onKeyDown={handleSubmit}
/>
</div>
<button onClick={handleBtnSubmit} ref={btnRef}>
<img src={sendButton} alt='Send Button' />
</button>
</div>
Child
const setMessage = (option) => {
parentData.setCurrentMessage(option);
// parentData.btnRef.current.click();
// console.log(parentData.currentMessage);
};
useEffect(() => {
console.log(setCurrentMessage.currentMessage);
// parentData.btnRef.current.click(); //tried this
// parentData.handleBtnSubmit(); //also also tried directly calling handleBtnSubmit();
//both are causing indefinite rerender
}, [parentData.currentMessage]); //tried this too
<li className='option' key={i} onClick={() => setMessage(option)}>
{option}
</li>
First, pass currentMessage as a separate property, instead of passing it within an object. Something like:
<Messages
messages={responses}
currentMessage={currentMessage}
parentData={{ setCurrentMessage, btnRef, handleBtnSubmit }}
/>;
Then try passing the currentMessage prop as a dependency into the useEffect as shown below:
useEffect(() => {
console.log(currentMessage);
setCurrentMessage.btnRef.current.click();
}, [currentMessage]); // child's message state
This way, the useEffect code is called only when the currentMessage changes, and not indefinitely.
Create another state that stores the full value onclick/submit. And use that value as a dependency to the useEffect()
const [messageText, setMessageText] = useState('');
useEffect(() => {
submitBtnRef.current.click();
}, [messageText]);

React prevent child update

I have a simple for with some fields in it, the fields being child components to that form. Each field validates its own value, and if it changes it should report back to the parent, which causes the field to re-render and lose focus. I want a behavior in which the child components do not update. Here's my code:
Parent (form):
function Form() {
const [validFields, setValidFields] = useState({});
const validateField = (field, isValid) => {
setValidFields(prevValidFields => ({ ...prevValidFields, [field]: isValid }))
}
const handleSubmit = (event) => {
event.preventDefault();
//will do something if all fields are valid
return false;
}
return (
<div>
<Title />
<StyledForm onSubmit={handleSubmit}>
<InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />
<Button type="submit" content="Enviar" maxWidth="none" />
</StyledForm>
</div>
);
}
export default Form;
Child (field):
function InputField(props) {
const [isValid, setValid] = useState(true);
const [content, setContent] = useState("");
const StyledInput = isValid ? Input : ErrorInput;
const validate = (event) => {
setContent(event.target.value);
setValid(stringValidator.validateField(event.target.value, props.isValidCondition))
props.reportState(props.name, isValid);
}
return (
<Field>
<Label htmlFor={props.name}>{props.name + ":"}</Label>
<StyledInput
key={"form-input-field"}
value={content}
name={props.name}
onChange={validate}>
</StyledInput>
</Field>
);
}
export default InputField;
By setting a key for my child element I was able to prevent it to lose focus when content changed. I guess I want to implement the shouldComponentUpdate as stated in React documentation, and I tried to implement it by doing the following:
Attempt 1: surround child with React.memo
const InputField = React.memo((props) {
//didn't change component content
})
export { InputField };
Attempt 2: intanciate child with useMemo on parent
const fooField = useMemo(<InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />, [fooRegex]);
return (
<div>
<Title />
<StyledForm onSubmit={handleSubmit}>
{fooField}
<Button type="submit" content="Enviar" maxWidth="none" />
</StyledForm>
</div>
);
Both didn't work. How can I make it so that when the child component isValid state changes, it doesn't re-render?
The problem is not that the component is re-rendering, it is that the component is unmounting given by this line:
const StyledInput = isValid ? Input : ErrorInput;
When react unmounts a component, react-dom will destroy the subtree for that component which is why the input is losing focus.
The correct fix is to always render the same component. What that means to you is based on how your code is structured, but I would hazard a guess that the code would end up looking a bit more like this:
function InputField(props) {
const [isValid, setValid] = useState(true);
const [content, setContent] = useState("");
const validate = (event) => {
setContent(event.target.value);
setValid(stringValidator.validateField(event.target.value, props.isValidCondition))
props.reportState(props.name, isValid);
}
return (
<Field>
<Label htmlFor={props.name}>{props.name + ":"}</Label>
<Input
valid={isValid} <-- always render an Input, and do the work of displaying error messages/styling based on the `valid` prop passed to it
value={content}
name={props.name}
onChange={validate}>
</Input>
</Field>
);
}
The canonical solution to avoiding rerendering with function components is React.useMemo:
const InputField = React.memo(function (props) {
// as above
})
However, because validateField is one of the props passed to the child component, you need to make sure it doesn't change between parent renders. Use useCallback to do that:
const validateField = useCallback((field, isValid) => {
setValidFields(prevValidFields => ({ ...prevValidFields, [field]: isValid }))
}, []);
Your useMemo solution should also work, but you need to wrap the computation in a function (see the documentation):
const fooField = useMemo(() => <InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />, [fooRegex]);

ReactJs - Input Component behaves strangely when its returned from a function

Scenario
I declared a react component that renders a simple html input tag.
const MyComponent = (props) => (
<input
defaultValue="test"
onChange={(e) => {
props.setTitle(e.target.value);
}}
/>
);
Then I declared a function that takes a setState as a parameter and returns that component with the setState inside the input's onChange.
const getComponent = (setTitle) => (props) => (
<input
defaultValue="test"
onChange={(e) => {
setTitle(e.target.value);
}}
/>
);
Then I called my function to get the component and to render it:
const Root = () => {
const [title, setTitle] = React.useState('');
const Component = getComponent(setTitle);
return (
<div>
<div>{title}</div>
<Component />{' '}
</div>
);
};
Expected:
The input element behaves normally, and changes its value
Reality:
The input loses focus after each character typed, and won't retain its value.
Here is a simple example of the error:
CodeSandbox
The reason this is happening is that when your code comes to this line:
const Component = getComponent(setTitle);
This generates new function (i.e. new instance) that is not rendered again, but mounted again. That is the reason you get unfocused from field.
There is no way you will make this work in this way, it's just not meant to be working like this. When you do it once when you are exporting it, than its ok, but every time === new instance.
If this is just an experiment that you are trying, than ok. But there is no reason not to pass setState as prop to that component.
I found a solution that let me use the function and keep the component from mounting each time.
If you put the function call inside the jsx, the component won't remount each render.
const Root = () => {
const [title, setTitle] = React.useState('');
const Component = ;
const someprops = {};
return (
<div>
<div>{title}</div>
{getComponent(setTitle)(someprops)}
</div>
);
};

React useState value not updated in ref callback

I have a functional component called SignUp it uses google recaptcha to secure the signup form.
Signup creates a ref pointing to the Recaptcha component and declares a callback function onResolved that points to a function method onRecaptchaResolved
The problem is that when onRecaptchaResolved is called after Recaptcha execution the value of our input is not the lastest state but the initial value set by useState
in our case "hi"
import React, { useState } from 'react';
import styled from 'styled-components';
import Recaptcha from 'react-google-invisible-recaptcha';
const Input = styled.input``
function SignUp({dispatch}) {
const [inputValue, setInputValue] = useState("hi");
let recaptcha = null; // this will be our ref
const formSubmit = () => {
recaptcha.execute()
}
const onRecaptchaResolved = ( recaptchaToken) => {
console.log(inputValue); // always logs; "hi"
}
return (
<>
<Input
placeholder="you#example.com"
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)
}
/>
<Recaptcha
ref={ ref => recaptcha = ref }
sitekey={ "{MY_SITE_KEY}" }
onResolved={recaptchaToken =>{ onRecaptchaResolved(recaptchaToken)} }
/>
<SubmitButton onClick={formSubmit}> Submit email</SubmitButton>
</>
)
}
How do I ensure that the input value read in onRecaptchaResolved is the updated value?
react-google-invisible-recaptcha seems to store the initial value provided in onResolved and won't update it unless <Recaptcha> is re-mounted. See
https://github.com/szchenghuang/react-google-invisible-recaptcha/blob/master/src/index.js#L41
The easiest way to confirm this is to set a key on <Recaptcha> that changes whenever inputValue changes.

Categories

Resources