I keep running into situations where the exhaustive-dep rule complains. In a simple below I want to reset the value whenever the props.id value changes:
const TestComponent = props => {
const [value, setValue] = useState(props.value);
useEffect(() => {
setValue(props.value);
}, [props.id]);
const saveValue = () => {
props.save(value);
}
return <input value={value} onChange={e => setValue(e.target.value)} onBlur={saveValue} />;
};
However, the lint rule wants me to also add props.value to the dependencies. If I do that it will effectively break the component since the effect will run on every change to the input and thus reset the state.
Is there a "right" way of achieving this behavior?
The way you're written your component is in a half controlled half uncontrolled style, which is difficult to do. I would recommend going either fully controlled, or fully uncontrolled.
A "controlled" component is one where it just acts on the props its given. It takes the value from its props and forwards it to the input, with no state. When the input changes, it calls an onChange prop. When the input blurs, it calls an onBlur prop. All the business logic is handled by the parent component.
The other option is an uncontrolled component, which is mostly what you have. The parent will pass in a prop for the initial value. I'd recommend naming it initialValue so it's clear this will only be checked once. After that, everything is managed by the child component. This means your useEffect goes away entirely:
const TestComponent = props = {
const [value, setValue] = useState(props.initialValue);
const saveValue = () => {
props.save(value);
}
return <input value={value} onChange={e => setValue(e.target.value)} onBlur={saveValue} />;
}
So how do you reset this then? With a key. The parent component can use a standard react key to tell react that it should unmount the old component and mount a new one:
const ParentComponent = props = {
// some code...
return <TestComponent key={id} initialValue={"whatever"} />
}
As long as the id doesn't change, the TestComponent will do its thing, starting with the initialValue passed in. When the id changes, the TestComponent unmounts and a new one mounts in its place, which will start with the initialValue it's given.
Related
When it comes to thinking about possibility of props changing, should I only care about parent component updating? Any other possible cases exist for props changing?
I can guess only one case like the code below. I would like to know if other cases exist.
const ParentComponent = () => {
const [message, setMessage] = useState('Hello World');
return (
<>
<button onClick={() => setMessage('Hello Foo')}>Click</button>
<ChildComponent message={message} />
</>
);
};
const ChildComponent = ({ message }: { message: string }) => {
return <div>{message}</div>;
};
Props can change when a component's parent renders the component again with different properties. I think this is mostly an optimization so that no new component needs to be instantiated.
Much has changed with hooks, e.g. componentWillReceiveProps turned into useEffect+useRef (as shown in this other SO answer), but Props are still Read-Only, so only the caller method should update it.
I have this component:
function Form() {
const [value, setValue] = useState('');
const hanleSubmit = (ev) => {
ev.preventDefault();
console.log('submit', value);
}
return (
<form onSubmit={props.onSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
and the following test:
import React from "react";
import { render, fireEvent } from "react-testing-library";
import Form from "./Form";
it("submits", () => {
const onSubmit = jest.fn();
const { getByText } = render(<Form />);
fireEvent.click(getByText("Add"));
expect(onSubmit).toHaveBeenCalled(); // test fail with 0 called
});
I know I gotta to pass this mock function as prop.
But I'd like to know if exist a way to test internal function like handleSubmit without pass as prop?
I'm using react-testing-library.
You can use jest.spyOn(console, 'log') and check the handleSubmit is triggered indirectly. In fact, handleSubmit should have some more meaningful logic, not just console.log(). We should test the meaningful logic. But for the code you posted, then only we can do is above.
You should test the behavior of the component rather than an implementation detail.
The behavior of the component is: when you trigger the event handler, some states may change, the component will re-render, UI structure change, assert UI changes.
The implementation detail is: assert some function is called or not. This makes test cases vulnerable.
Test behavior or function, you don't need to pay attention to implementation details, regardless of the implementation details, we only test the final result.
If your component is just a presentation component with no state, just perform a Snapshot test to check the UI hierarchy.
If there are states, you need to test different states of the component.
If the state is promoted to the parent, then you test the parent and the state of the child on the UI when the parent state changes
Passing the mock onSubmit as a prop is a simple and common way.
If you want to handle some logic in Form component and call the onSubmit function passed from parent component in the same time.
You should write it like this:
const hanleSubmit = (ev) => {
ev.preventDefault();
// some logic for Form component itself, you should test these logic when you test Form component rather than `props.onSubmit()`.
console.log('submit', value);
// Parent component only care about the submitted value.
props?.onSubmit(value)
}
I'm having trouble navigating these concepts where a child keeps re-rendering because I'm passing it a function from the parent. This parent function references an editor's value, draftjs.
function Parent() {
const [doSomethingValue, setDoSomethingValue] = React.useState("");
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const editorRef = useRef<HTMLInputElement>(null);
const doSomething = () => {
// get draftjs editor current value and make a fetch call
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
// do something with userResponse
setDoSomethingValue(someValue);
}
return (
<React.Fragment>
<Child doSomething={doSomething} />
<Editor
ref={editorRef}
editorState={editorState}
onChange={setEditorState}
placeholder="Start writing..." />
<AnotherChild doSomethingValue={doSomethingValue}
<React.Fragment>
}
}
My Child component is simply a button that calls the parent's doSomething and thats it.
doSomething does its thing and then makes a change to the state which is then passed to AnotherChild.
My problem is that anytime the editorState is updated (which is every time you type within the editor), my Child component re-renders. Isn't that unnecessary? And if so, how could I avoid this?
If I was passing my Child component a string and leveraged React.Memo, it does not re-render unless the string changes.
So what am I missing with passing a function to the child? Should my child be re-rendering everytime?
React works on reference change detection to re-render components.
Child.js: Wrap it under React.memo so it becomes Pure Component.
const Child = ({doSomething}) => <button onClick={doSomething}>Child Button Name</button>;
export default React.memo(Child);
Parent.js -> doSomething: On every (re)render, callbacks are also recreated. Make use of useCallback so that your function is not recreated on every render.
const doSomething = React.useCallback(() => {
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
setDoSomethingValue(someValue);
}, [editorState]);
Side Note
On broader lines, memo is HOC and makes a component Pure Component. useMemo is something which cache the output of the function. Whereas useCallback caches the instance of the function.
Hope it helps.
If you do not want your component to be re-rendered every time your parent is re-rendered, you should take a look at useMemo.
This function will only recalculate its value (in your case, your component) whenever its second argument changes (here, the only thing it depends on, doSomething()).
function Parent() {
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const editorRef = useRef<HTMLInputElement>(null);
const doSomething = () => {
// get draftjs editor current value and make a fetch call
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
// do something with userResponse
}
const childComp = useMemo(() => <Child doSomething={doSomething} />, [doSomething])
return (
<React.Fragment>
{childComp}
<Editor
ref={editorRef}
editorState={editorState}
onChange={setEditorState}
placeholder="Start writing..." />
<React.Fragment>
}
}
If doSomething does not change, your component does not re-render.
You may also want to use useCallback for your function if it is doing heavy calculations, to avoid having it re-compiled every time your component renders: https://reactjs.org/docs/hooks-reference.html#usecallback
Take a look into PureComponent, useMemo, or shouldComponentUpdate
I would also add, instead of passing down the function to do the rendering of a top level component, pass the value and define the function later down in the component tree.
if you want to avoid unnecessary re-renders, you can use React.memo and the hook useCallback. Take a look at the following sandbox.
The button1 is always re-rendered because it take a callback that is not memoized with useCallback and the button2 is just rendered the first time even if the state of the parent has changed (take a look at the console to check the re-renders). You have to use React.memo in the Child component that is the responsible to render the buttons.
I hope it helps.
So we all know uncontrolled components are usually a bad thing, which is why we usually want to manage the state of an input (or group of inputs) at a higher-level component, usually some kind of container. For example, a <Form /> component manages state and passes down state as values to its <Input /> components. It also passes down functions such as handleChange() that allow the Input to update the state.
But while implementing my own <NumericInput /> component, it got me thinking that fundamentally this component is not self-reliant. It's reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my <NumericInput />, two additional functions to control the stepper arrows.
If I (or someone who took over my code) wanted to use this <NumericInput />, but they forget to run to a different container component and copy the stepUp() and stepDown() functions to pass down as props, then there will just be two non-functional arrows. I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive. Am I thinking about this incorrectly, or is there a better way of managing this?
I recognize this is more of a theory/design question, but I'm including my code below for reference:
NumericInput:
const NumericInput = ({label, stepUp, stepDown, ...props}) => (
<>
{label && <Label>{label}</Label>}
<InputContainer>
<Input type={props.type || "number"} {...props} />
<StepUp onClick={stepUp}>
//icon will go here
</StepUp>
<StepDown onClick={stepDown}>
//icon will go here
</StepDown>
</InputContainer>
</>
);
Form.js
const Form = (props) => {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
}
const stepUp = () => {
setValue(value++);
}
const stepDown = () => {
setValue(value--);
}
return (
<NumericInput
stepUp={stepUp}
stepDown={stepDown}
handleChange={handleChange}
label="Numeric"
)
}
Let's try to boil down your questions a bit:
[NumericInput] is reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my , two additional functions to control the stepper arrows.
The only repetition you have is to define a value and a handleChange callback property for <NumericInput />. The value prop contains your numeric input value and is passed in from the parent container component owning that state. You need the callback to trigger a change request from the child (controlled component). That is the ususal approach, when you need to lift state up or divide your components in container and presentational components.
stepUp/stepDown methods are an implementation detail of NumericInput, so you can remove them from Form. Form just wants to know, if a value change has been triggered and what the new changed numeric value is.
Concerning where to store the state: There is probably a reason, why you want to save the state in the Form component. The user enters some input in (possibly) multiple fields before pressing the submit button. In this moment your form needs all the state of its child fields to trigger further processing based on the given input. And the idiomatic React way is to lift state up to the form to have the information.
I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive.
A presentational component is rather less error prone and is independent, because it doesn't care about state! For sure, you write a bit more boilerplate code by exposing callbacks and value props in child. The big advantage is, that it makes the stateless components far more predictable and testable and you can shift focus on complex components with state, e.g. when debugging.
In order to ease up props passing from Form to multiple child components, you also could consolidate all event handlers to a single one and pass state for all inputs. Here is a simple example, how you could handle the components:
NumericInput.js:
const NumericInput = ({ label, value, onValueChanged }) => {
const handleStepUp = () => {
onValueChanged(value + 1);
};
const handleStepDown = () => {
onValueChanged(value - 1);
};
return (
<>
...
<Input value={this.state.value} />
<StepUp onClick={handleStepUp}></StepUp>
<StepDown onClick={handleStepDown}></StepDown>
</>
);
};
Form.js:
const Form = (props) => {
const [value, setValue] = useState(0);
const handleValueChanged = (value) => {
setValue(value);
}
return (
<NumericInput
onValueChanged={handleValueChanged}
value={value}
label="Numeric"
)
}
Move the setValue function to the NumericInput component
Manage your state through the component.
return (
<NumericInput
setValue={setValue}
label="Numeric"
/>
)
I would recommend you use hooks
We can prevent unnecessary calculations for useEffect with an empty array as the second argument in the hook:
// that will be calculated every single re-rendering
useEffect(() => {...some procedures...})
// that will be calculated only after the first render
useEffect(() => {...some procedures...}, [])
But, for the useContext hook we can't do like above, provide the second argument.
Also, we can't wrap useContext by useCallback, useMemo.
For instance, we have a component:
const someComponent = () => {
const contextValue = useContext(SomeContext);
const [inputValue, setInputValue] = useState('');
return (
<Fragment>
<input value={inputValue} onChange={onInputChange} />
<span>{contextValue}</span>
</Fragment>
)
The problem is that every single typing will launch re-rendering and we will have unnecessary useContext re-rendering each time. One of the decision is brake the component on two:
const WithContextDataComponent = () => {
const contextValue = useContext(SomeContext);
return <JustInputComponent contextValue={contextValue} />
const JustInputComponent = (props) => {
const [inputValue, setInputValue] = useState('');
return <input value={inputValue} onChange={onInputChange} />
So, now the problem is disappeared, but two components we have, though. And in the upper component instead <SomeComponent /> we should import <WithContextDataComponent />, that a little bit ugly, I think.
Can I stop the unnecessary re-rendering for useContext without splitting into two components?
From React Hooks API Reference:
https://reactjs.org/docs/hooks-reference.html#usecontext
useContext
const value = useContext(MyContext);
Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest above the calling component in the tree.
When the nearest above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider.
As you can see from the documentation, the useContext() hook will only cause a re-render of your component if the values that it provides change at some point. Ans this is probably your intended behavior. Why would you need stale data in your context hook?
When your component re-render by itself, without changes in the context, the useContext() line will simply give back the same values that it did on the previous render.
It seems that you're using the useContext() hook the way it it meant to be used. I don't see anything wrong with it.