useCallback necessary within React.memo? - javascript

Consider this example:
import React, { useCallback } from 'react';
type UserInputProps = {
onChange: (value: string) => void;
};
const UserInput = React.memo(({ onChange }: UserInputProps) => {
// Is this `useCallback` redundant?
const handleChange = useCallback(
(event) => {
onChange(event.target.value);
},
[onChange]
);
return <input type="text" onChange={handleChange} />;
});
export default UserInput;
My questions are:
When the props consist of only onChange and no other elements, is the useCallback unnecessary in this case, since the entire component is already memo-ed based on onChange?
If we add an additional prop (say a value for the initial value for the <input>), then I think useCallback becomes useful, since otherwise handleChange will be recreated even if onChange doesn't change but value is changed. Is that correct?

When the props contains only onChange and no other elements, is the useCallback unnecessary in this case, since the entire component is already memo-ed based on onChange?
It's memoized based on all props, not just onChange. Yes, useCallback is unnecessary there.
If we add an additional prop (say a value for the initial value for the <input>), then I think useCallback becomes useful, since otherwise handleChange will be recreated even if onChange doesn't change but value is changed. Is that correct?
If you want to update value and not update onChange when updating the input, you'd add value to your props and continue to use useCallback for your handleChange (if you like, so that React doesn't have to swap in a new event handler when it's setting the value; my impression is that's often overkill). E.g.:
const UserInput = React.memo(({ onChange, value }: UserInputProps) => {
const handleChange = useCallback(
(event) => {
onChange(event.target.value);
},
[onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
});
That will use the latest value and reuse the previous handleChange if the onChange prop hasn't changed.
You might not keep React.memo in that case, if you're expecting that value will get changed as a result of the onChange. That is, if you expect the common case to be that value will be different each time your component is called, then React.memo's check of the props is extra work you might not keep:
const UserInput = ({ onChange, value }: UserInputProps) => {
const handleChange = useCallback(
(event) => {
onChange(event.target.value);
},
[onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
};
But you can keep it if you want React.memo to keep checking the props and not call your function when neither prop changes.
If you just want to supply the initial value to your component and then control it locally, you could continue to use React.memo (since it still renders the same thing for the same input props), in which case you don't need useCallback:
const UserInput = React.memo(({ onChange, initialValue }: UserInputProps) => {
const [value, setValue] = useState(initialValue);
const handleChange = (event) => {
setValue(event.target.value);
onChange(event.target.value);
};
return <input type="text" value={value} onChange={handleChange} />;
});
That will only get called when onChange or initialValue changes. You could use also useCallback there in order to, again, only update the onChange on the input when the onChange prop changes, to avoid having React remove the old handler and set the new one when just the value changes:
const UserInput = React.memo(({ onChange, initialValue }: UserInputProps) => {
const [value, setValue] = useState(initialValue);
const handleChange = useCallback(
(event) => {
setValue(event.target.value);
onChange(event.target.value);
},
[onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
});
Side note: One thing to remember is that a new handleChange function is created every time your component function is called, even when you're using useCallback. It has to be, so it can be passed into useCallback as an argument. The only difference is whether you use that new function, or the original one that was created the first time (the result of useCallback). I think the reason for reusing the first one that was created for a given set of dependencies is to minimize changes passed to child components.

When the props consist of only onChange and no other elements, is the useCallback unnecessary in this case, since the entire component is already memo-ed based on onChange?
No, it might be necessary, memo and useCallback don't serve the same purpose.
Without useCallback you may have "heavy computation" on every render:
"heavy computation" refers to a generic case and not to this specific example where you only pass the event's value.
const UserInput = React.memo(({ onChange = () => {} }) => {
// Uncomment for memoization
// Note that you can implement useCallback with useMemo
// const handleChange = useMemo(() => {
// console.log("heavy computation memoized");
// return event => {
// onChange(event.target.value);
// };
// }, [onChange]);
const handleChange = event => {
// Here we can have some heavy computation
// Not related to this specific usecase
console.log("heavy computation on every render");
onChange(event.target.value);
};
return <input type="text" onChange={handleChange} />;
});
If we add an additional prop (say a value for the initial value for the ), then I think useCallback becomes useful, since otherwise handleChange will be recreated even if onChange doesn't change but value is changed. Is that correct?
If you are going to use controlled component (due to use of value prop of input), the initialValue will be initialized once, so it pretty useless trying to memorize it (for what purpose?):
const UserInputWithValue = ({ onChange, initialValue }) => {
// initilized once, no need to memoize
const [value,setValue] = useState(initialValue);
const handleChange = useCallback(
(event) => {
setValue(event.target.value)
onChange(event.target.value);
},
[onChange]
);
// Controlled
return <input type="text" value={value} onChange={handleChange} />;
};

Related

In React when in onChange function how to update another custom component's state and pass a value in?

In a React app currently there is a drop down list that has an onChange event that calls a function. In that function (when the users selects a different choice in the ddl) what I would like to achive is to update another custom component & pass a value into that component.
So in the front end there is a simple drop down:
<Dropdown
value={selectedOption}
options={dropDownOptions}
onChange={onChange}
/>
Then there is an onChange function that gets fired when the drop down gets selected:
const onChange = React.useCallback(
e => {
const optionId = e.target.value;
const optionData = keyedOptions[optionId];
// refresh DownloadSelector custom component
// something like this which doesn't work {optionData.id && <DownloadSelector eventId={optionData.id} />} }
Also I am able to import the custom component at the top of the file normally such as:
import { DownloadSelector } from '../../../SearchAndSort/DownloadSelector';
The custom component when defining it has a value passed in like so:
export const DownloadSelector = ({eventId}) => {
If the whole page refreshes the custom DownloadSelector component will get uploaded. I would like that to happen in the onChange.
How in the onChange function can we update/reload/setState/refresh the DownloadSelector component?
const [eventId, setEventId] = React.useState()
const onChange = React.useCallback(() {
// ....
// replace newState with a updated value
setEventId( newState )
}, [])
return <DownloadSelector eventId={eventId} />
In DownloadSelector do something like this:
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
}
const DownloadSelector = ({eventId}) => {
const forceUpdate = useForceUpdate();
// ....
return <Dropdown
value={selectedOption}
options={dropDownOptions}
onChange={onChange}
forceUpdate={forceUpdate}
/>
}
And then in onChange in Dropdown Component:
const onChange = React.useCallback(() {
// ....
forceUpdate()
}, [])
The answer for my question is in the custom component that I wanted to rerender when the drop down ( a different component) is changed is to define the variables that are changing in the custom component DownloadSelector in state (not a local let) such as:
const [isMyVar] = useState(false);
Then when isMyVar is updated the state changed & the component rerenders. The answer was not (what I originally thought) was to have the code in the onChange event of the 2nd component. I appreciate #Hostek for your help.

Value of React input component cannot be changed if "value" field is used

I'm using material-UI for my React app.
I want to delete value inserted to input field by clicking another component.
The issue is that the OutlinedInput field works properly only when it does not have value property. In case it is added, the value from value is shown inside the OutlinedInput field and no another value can be inserted. However, I cannot delete the value of OutlinedInput by clicking another React component if OutlinedInput doesn't have value property.
const Search = (props: any) => {
const searchedMovie = useSelector(
(state: RootState) => state.searchedMovie
);
const isMovieOpened = useSelector(
(state: RootState) => state.isMoviePageOpened
);
const dispatch: Dispatch<any> = useDispatch();
let history = useHistory();
const fetchMoviesList = (event: any) => {
const searchedMovieValue = event.target.value;
dispatch(changeSearchedMovie(searchedMovieValue));
window.scrollTo(0, 0);
if (isMovieOpened === true) {
dispatch(isMoviePageOpened(false));
history.push("/");
}
}
const handleKeyPress = (event: any) => {
if (event.keyCode === 13) {
fetchMoviesList(event);
}
}
useEffect(()=>{
if (searchedMovie) {
fetchSearchedMovie(searchedMovie)
.then((res) => dispatch(addHomePageMovies(res)))
.catch(() => dispatch(addHomePageMovies([])));
} else {
fetchMovies('1')
.then((res) => dispatch(addHomePageMovies(res)))
.catch(() => dispatch(addHomePageMovies([])));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchedMovie]);
return (
<>
<OutlinedInput
color="secondary"
className="seach-field"
type="string"
onBlur={fetchMoviesList}
onKeyDown={handleKeyPress}
placeholder="Search"
/>
</>
);
}
export default Search;
How can I solve it? Thanks!
Generally, input components in react can follow two patterns: controlled components or uncontrolled components.
In a controlled component, the parent stores the current value as state and provides it via a prop to the input component. It also provides an onChange handler to update the state on the parent whenever the value changes from the child.
In an uncontrolled component, the state is managed internally by the component.
Many components (including material UI's) offer the ability to be either controlled or uncontrolled, by allowing you to pass both or neither of value/onChange. Your current solution is a mix of both, which is a mistake.
You should add a useState hook to the Search component to store the input value (a string) and pass it to the OutlinedInput. You should also pass an onChange callback which sets this state to event.target.value.

Implementing Lodash's debounce in a ReactJS functional component

I'm trying to debounce text input field change with Lodash's debounce function.
import React from "react";
import debounce from 'lodash.debounce';
const Input = () => {
const onChange = debounce((e) => {
const { value } = e.target;
console.log('debounced value', value)
}, 1000)
return (
<input type="text" onChange={ onChange } />
)
};
The code above throws the following errors:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
Uncaught TypeError: Cannot read property 'value' of null
What is the correct implementation?
When to Use Refs There are a few good use cases for refs:
Managing focus, text selection, or media playback.
Triggering imperative animations.
Integrating with third-party DOM libraries.
Avoid using refs for anything that can be done declaratively.
Refs and the DOM
The way you defined Input, I am assuming it would be used in many places. So, I would do this:
import React from "react";
import debounce from 'lodash.debounce';
const Input = () => {
// Debounced function
// `printValue` only cares about last state of target.value
const printValue = debounce(value => console.log(value), 1000);
// Event listener called on every change
const onChange = ({ target }) => printValue(target.value);
return <input type="text" onChange={ onChange } />;
};
The fix was not to retrieve the value from the event but from the input directly with a ref.
import React, { useRef } from "react";
import debounce from 'lodash.debounce';
const Input = () => {
const input = useRef( null )
const onChange = debounce(() => {
console.log('debounced value', input.current.value)
}, 1000)
return (
<input ref={ input } type="text" onChange={ onChange } />
)
};
If we don't need to rely on any internal component state/props in our onChange handler, we should be able to lift the debounced function call out of the component (to avoid it being recreated on each render), which means we don't need to use refs or useCallback:
import debounce from "lodash.debounce";
import Input from "./Input";
const handleChange = debounce(({ target }) => {
console.log(target.value);
}, 500);
export default function App() {
return (
<div className="App">
<label htmlFor="input-first-name">First name</label>
<Input id="input-first-name" name="firstName" onChange={handleChange} />
</div>
);
}
CodeSandbox: https://codesandbox.io/s/react-input-debounce-feleu9?file=/src/App.js&expanddevtools=1

React object as useEffect dependency triggers callback on every render

I have a relatively simple React hook to debounce a value. The only twist is that I want to provide a callback which gets triggered whenever the debounced value changes. I'm aware that I could just have another useEffect inside App which listens for changes on debouncedValue, but I'd like to integrate this behavior into the reusable hook.
const useDebounce = (value, delay, { onChange }) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(value);
onChange(value);
}, delay);
return () => {
clearTimeout(timeout);
};
}, [value, delay, onChange]);
return debouncedValue;
};
const App = () => {
const [value, setValue] = useState("");
const debouncedValue = useDebounce(value, 750, { onChange: (value) => console.log(value) });
return (
<div style={{ margin: 50 }}>
<input value={value} onChange={(event) => setValue(event.target.value)} />
<p>{value}</p>
<p>{debouncedValue}</p>
</div>
);
};
Now to the issue I have is that the onChange fires twice for every debounce. Apparently the onChange callback must be part of the dependency array of useEffect, even if I don't care when it updates. But this creates the problem than on every render a new object for { onChange: (value) => console.log(value) } gets created therefore reference equality thinks the useEffect dependency has changed. I could just create the object outside of my component, but that would also complicate the API of my hook.
So, my question: what is the easiest way to solve this problem? Ideally without pulling in any 3rd party packages. The fix I can think of would be to write a custom useEffect hook that does deep comparisons, but I hope someone can point me to a simpler solution.

Use dynamically created react components and fill with state values

Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));

Categories

Resources