What is the different between useImperativeHandle and useRef? - javascript

As I understand, useImperativeHandle helps parent component able to call function of its children component. You can see a simple example below
const Parent = () => {
const ref = useRef(null);
const onClick = () => ref.current.focus();
return <>
<button onClick={onClick} />
<FancyInput ref={ref} />
</>
}
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
but it can be easy achieved by using only useRef
const Parent = () => {
const ref = useRef({});
const onClick = () => ref.current.focus();
return <>
<button onClick={onClick} />
<FancyInput ref={ref} />
</>
}
function FancyInput(props, ref) {
const inputRef = useRef();
useEffect(() => {
ref.current.focus = inputRef.current.focus
}, [])
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
So what is the true goal of useImperativeHandle. Can someone give me some advices?. Thank you

Probably something similar to the relationship between useMemo and useCallback where useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Sometimes there is more than one way to accomplish a goal.
I'd say in the case of useImperativeHandle the code can be a bit more succinct/DRY when you need to expose out more than an single property.
Examples:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
property,
anotherProperty,
... etc ...
}), []); // use appropriate dependencies
...
}
vs
function FancyInput(props, ref) {
const inputRef = useRef();
useEffect(() => {
ref.current.focus = inputRef.current.focus;
ref.current.property = property;
ref.current.anotherProperty = anotherProperty;
... etc ...
}, []); // use appropriate dependencies
...
}
Not a big difference, but the useImperativeHandle is less code.

it can be easy achieved by using only useRef
No, you need at least another useEffect or probably better useLayoutEffect?
And even then it does a teeny tiny bit more than your code.
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
is more likely equivalent to:
// using a function.
// no need to create this object over and over if there is no `ref`,
// or no need to update the `ref`.
const createRef = () => ({
focus: () => {
inputRef.current.focus();
}
});
useLayoutEffect(() => {
// refs can be functions!
if (typeof ref === "function") {
ref(createRef());
// when the ref changes, the old one is updated to `null`.
// Same on unmount.
return () => {
ref(null);
}
}
// and the same thing again for ref-objects
if (typeof ref === "object" && ref !== null && "current" in ref) {
ref.current = createRef();
return () => {
ref.current = null;
}
}
}, [ref]);

Related

Send a http request to the server with a delay due to content changes [duplicate]

I have a here a input field that on every type, it dispatches a redux action.
I have put a useDebounce in order that it won't be very heavy. The problem is that it says Hooks can only be called inside of the body of a function component. What is the proper way to do it?
useTimeout
import { useCallback, useEffect, useRef } from "react";
export default function useTimeout(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const set = useCallback(() => {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
}, [delay]);
const clear = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
useEffect(() => {
set();
return clear;
}, [delay, set, clear]);
const reset = useCallback(() => {
clear();
set();
}, [clear, set]);
return { reset, clear };
}
useDebounce
import { useEffect } from "react";
import useTimeout from "./useTimeout";
export default function useDebounce(callback, delay, dependencies) {
const { reset, clear } = useTimeout(callback, delay);
useEffect(reset, [...dependencies, reset]);
useEffect(clear, []);
}
Form component
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const { handleChangeProductName = () => {} } = props;
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
useDebounce(() => handleChangeProductName(e.target.value), 1000, [
e.target.value,
]);
}}
/>
);
}
I don't think React hooks are a good fit for a throttle or debounce function. From what I understand of your question you effectively want to debounce the handleChangeProductName function.
Here's a simple higher order function you can use to decorate a callback function with to debounce it. If the returned function is invoked again before the timeout expires then the timeout is cleared and reinstantiated. Only when the timeout expires is the decorated function then invoked and passed the arguments.
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
Example usage:
export default function ProductInputs({ handleChangeProductName }) {
const debouncedHandler = useCallback(
debounce(handleChangeProductName, 200),
[handleChangeProductName]
);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandler(e.target.value);
}}
/>
);
}
If possible the parent component passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.
Taking a look at your implementation of useDebounce, and it doesn't look very useful as a hook. It seems to have taken over the job of calling your function, and doesn't return anything, but most of it's implementation is being done in useTimeout, which also not doing much...
In my opinion, useDebounce should return a "debounced" version of callback
Here is my take on useDebounce:
export default function useDebounce(callback, delay) {
const [debounceReady, setDebounceReady] = useState(true);
const debouncedCallback = useCallback((...args) => {
if (debounceReady) {
callback(...args);
setDebounceReady(false);
}
}, [debounceReady, callback]);
useEffect(() => {
if (debounceReady) {
return undefined;
}
const interval = setTimeout(() => setDebounceReady(true), delay);
return () => clearTimeout(interval);
}, [debounceReady, delay]);
return debouncedCallback;
}
Usage will look something like:
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const handleChangeProductName = useCallback((value) => {
if (props.handleChangeProductName) {
props.handleChangeProductName(value);
} else {
// do something else...
};
}, [props.handleChangeProductName]);
const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandleChangeProductName(e.target.value);
}}
/>
);
}
Debouncing onChange itself has caveats. Say, it must be uncontrolled component, since debouncing onChange on controlled component would cause annoying lags on typing.
Another pitfall, we might need to do something immediately and to do something else after a delay. Say, immediately display loading indicator instead of (obsolete) search results after any change, but send actual request only after user stops typing.
With all this in mind, instead of debouncing callback I propose to debounce sync-up through useEffect:
const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);
useEffect(() => {
if (isValueSettled) {
props.onChange(text);
}
}, [text, isValueSettled]);
...
<input value={value} onChange={({ target: { value } }) => setText(value)}
And useIsSetlled itself will debounce:
function useIsSettled(value, delay = 500) {
const [isSettled, setIsSettled] = useState(true);
const isFirstRun = useRef(true);
const prevValueRef = useRef(value);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
setIsSettled(false);
prevValueRef.current = value;
const timerId = setTimeout(() => {
setIsSettled(true);
}, delay);
return () => { clearTimeout(timerId); }
}, [delay, value]);
if (isFirstRun.current) {
return true;
}
return isSettled && prevValueRef.current === value;
}
where isFirstRun is obviously save us from getting "oh, no, user changed something" after initial rendering(when value is changed from undefined to initial value).
And prevValueRef.current === value is not required part but makes us sure we will get useIsSettled returning false in the same render run, not in next, only after useEffect executed.

Extend React forwardRef component with methods

I want to create an extended component with some additional functions added.
Let's say I have an ExtendedButton component which has a button that is forwardRef:ed, but which also has a doubleClick method. I know this is a silly example, but something like this:
const ExtendedButton = forwardRef<HTMLButtonElement, React.HTMLAttributes<HTMLButtonElement>>((props, ref) => {
const btnRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => btnRef?.current as HTMLButtonElement);
const doubleClick = () => {
btnRef.current?.click();
btnRef.current?.click();
};
return <button {...props} ref={btnRef}></button>;
});
I want to be able to get the doubleClick method, as well as all the methods on the button, from a consumer component like this:
export const Consumer = () => {
const ref = useRef<HTMLButtonElement>(null);
ref.current.doubleClick();
ref.current.click();
return <ExtendedButton ref={ref}></ExtendedButton>;
};
I feel I should probably remove the forwardRef so the ref is pointing to ExtendedButton instead of button, but how can I get the button methods then?
Thanks!
useImperativeHandle should expose all the methods you want to access:
type ExtendedButtonType = HTMLButtonElement & { doubleClick: () => void }
const ExtendedButton = forwardRef<ExtendedButtonType, React.HTMLAttributes<HTMLButtonElement>>(
(props, ref) => {
const btnRef = useRef<HTMLButtonElement>(null)
const doubleClick = (): void => {
btnRef.current?.click()
btnRef.current?.click()
}
useImperativeHandle(
ref,
() =>
({
...btnRef.current,
doubleClick,
} as ExtendedButtonType),
)
return <button {...props} ref={btnRef} />
},
)
export const Consumer: FC = () => {
const ref = useRef<ExtendedButtonType>(null)
ref.current?.doubleClick()
ref.current?.click()
return <ExtendedButton ref={ref} />
}
add the method inside the useImperativeHandle
const ExtendedButton = forwardRef<HTMLButtonElement, React.HTMLAttributes<HTMLButtonElement>>((props, ref) => {
const btnRef = useRef<HTMLButtonElement>( );
useImperativeHandle(ref, () => ({
...btnRef.current,
doubleClick: () => {
btnRef.current?.click();
btnRef.current?.click();
};
}));
return <button {...props} ref={btnRef}></button>;
});

What approach can I use to wait for all child callbacks to complete?

I'm trying to figure out an architecture to allow a parent component, to wait for all the child components to finish rendering and then do some work. Unfortunately in this case I need to do the rendering outside of React and it's done asynchronously instead.
This makes things a little complex. So in my example I want the doSomethingAfterRender() function in the ParentComponent to be called once, after all the ChildComponent customRender calls have completed.
I do have one potential solution, though it doesn't feel very clean which is to use a debounce on the doSomethingAfterRender() function. I'd much rather use a more deterministic approach to only calling this function once if possible.
I'm wondering if anyone has a better suggestion for handling this?
ParentComponent.js
const Parent = (props) => {
// This is the function I need to call
const doSomethingAfterRender = useCallback(
async (params) => {
await doSomething();
},
);
// Extend the child components to provide the doSomethingAfterRender callback down
const childrenWithProps = React.Children.map(props.children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { doSomethingAfterRender });
}
return child;
});
return (
<React.Fragment>
<...someDOM....>
{childrenWithProps}
</React.Fragment>
);
}
ChildComponent.js (this is actually a HoC)
const withXYZ = (WrappedComponent) =>
({ doSomethingAfterRender, ...props }) => {
// I need to wait for this function to complete, on all child components
const doRender = useCallback(
async () => {
await customRender();
// Call the doSomething...
if (doSomethingAfterRender) {
doSomethingAfterRender();
}
},
[doSomethingAfterRender]
);
return (
<React.Fragment>
<... some DOM ...>
<WrappedComponent {...props} renderLoop={renderLoop} layer={layer} />
</React.Fragment>
);
};
App.js
const Child = withXYZ(CustomWrappedComponent);
const App = () => {
return {
<ParentComponent>
<Child />
<Child />
<Child />
</ParentComponent>
};
}
If I understood correctly. I would do something like: useState with useRef.
This way I would trigger only once the Parent and that is when all Child components have finished with their respective async tasks.
Child.js
const child = ({ childRef, updateParent }) => {
const [text, setText] = useState("Not Set");
useEffect(() => {
if (typeof childRef.current !== "boolean") {
const display = (childRef.current += 1);
setTimeout(() => {
setText(`Child Now ${display}`);
if (childRef.current === 2) {
updateParent(true);
childRef.current = false;
}
}, 3000);
}
}, []);
return (
<>
<div>Test {text}</div>
</>
);
}
const Parent = ()=>{
const childRef = useRef(0);
const [shouldUpdate, setShouldUpdate] = useState(false);
useEffect(() => {
if (shouldUpdate) {
console.log("updating");
}
}, [shouldUpdate]);
return (
<div className="App">
{childRef.current}
<Child childRef={childRef} updateParent={setShouldUpdate} />
<Child childRef={childRef} updateParent={setShouldUpdate} />
<Child childRef={childRef} updateParent={setShouldUpdate} />
</div>
);
}

How to use useeffect hook in react?

i want to return a function that uses useEffect from the usehook and i am getting error "useeffect is called in a function which is neither a react function component or custom hook.
what i am trying to do?
i have addbutton component and when user clicks add button i want to call the function requestDialog.
below is my code within addbutton file
function AddButton () {
const count = useGetCount();
const requestDialog = useRequestDialog(); //using usehook here
const on_add_click = () => {
requestDialog(count); //calling requestDialog here
}
return (
<button onClick={on_add_click}>add</button>
);
}
interface ContextProps {
trigger: (count: number) => void;
}
const popupContext = React.createContext<ContextProps>({
trigger: (availableSiteShares: number) => {},
});
const usePopupContext = () => React.useContext(popupContext);
export const popupContextProvider = ({ children }: any) => {
const [show, setShow] = React.useState(false);
const limit = 0;
const dismiss = () => {
if (show) {
sessionStorage.setItem(somePopupId, 'dismissed');
setShow(false);
}
};
const isDismissed = (dialogId: string) =>
sessionStorage.getItem(dialogId) === 'dismissed';
const context = {
trigger: (count: number) => {
if (!isDismissed(somePopupId) && count <= limit) {
setShow(true);
} else if (count > limit) {
setShow(false);
}
},
};
return (
<popupContext.Provider value={context}>
{children}
{show && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
};
export function useRequestDialog(enabled: boolean,count: number) {
return function requestDialog() { //here is the error
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}
}, [count, trigger]);
}
How to solve the error ""useEffect is called in a function which is neither a react function component or custom hook."
i am not knowing how to use useeffect and the same time use it in the addbutton component.
could someone help me with this. thanks
useEffect method is like, useEffect(() => {}, []), But your usage in requestDialog is wrong. Try changing with following.
function requestDialog() {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [count, trigger]);
}

useEffect with debounce

I'm trying to create an input field that has its value de-bounced (to avoid unnecessary server trips).
The first time I render my component I fetch its value from the server (there is a loading state and all).
Here is what I have (I omitted the irrelevant code, for the purpose of the example).
This is my debounce hook:
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
(I got this from: https://usehooks.com/useDebounce/)
Right, here is my component and how I use the useDebounce hook:
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [commitsCount, setCommitsCount] = useState(0);
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
}, [props.title]);
useEffect(() => {
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setCommitsCount(commitsCount + 1);
}
}, [debouncedTitle, lastCommittedTitle, commitsCount]);
return (
<div className="example-input-container">
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div>Last Committed Value: {lastCommittedTitle}</div>
<div>Commits: {commitsCount}</div>
</div>
);
}
Here is the parent component:
function App() {
const [title, setTitle] = useState("");
useEffect(() => {
setTimeout(() => setTitle("This came async from the server"), 2000);
}, []);
return (
<div className="App">
<h1>Example</h1>
<ExampleTitleInput title={title} />
</div>
);
}
When I run this code, I would like it to ignore the debounce value change the first time around (only), so it should show that the number of commits are 0, because the value is passed from the props. Any other change should be tracked. Sorry I've had a long day and I'm a bit confused at this point (I've been staring at this "problem" for far too long I think).
I've created a sample:
https://codesandbox.io/s/zen-dust-mih5d
It should show the number of commits being 0 and the value set correctly without the debounce to change.
I hope I'm making sense, please let me know if I can provide more info.
Edit
This works exactly as I expect it, however it's giving me "warnings" (notice dependencies are missing from the deps array):
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [commitsCount, setCommitsCount] = useState(0);
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
// I added this line here
setLastCommittedTitle(props.title || "");
}, [props]);
useEffect(() => {
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setCommitsCount(commitsCount + 1);
}
}, [debouncedTitle]); // removed the rest of the dependencies here, but now eslint is complaining and giving me a warning that I use dependencies that are not listed in the deps array
return (
<div className="example-input-container">
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div>Last Committed Value: {lastCommittedTitle}</div>
<div>Commits: {commitsCount}</div>
</div>
);
}
Here it is: https://codesandbox.io/s/optimistic-perlman-w8uug
This works, fine, but I'm worried about the warning, it feels like I'm doing something wrong.
A simple way to check if we are in the first render is to set a variable that changes at the end of the cycle. You could achieve this using a ref inside your component:
const myComponent = () => {
const is_first_render = useRef(true);
useEffect(() => {
is_first_render.current = false;
}, []);
// ...
You can extract it into a hook and simply import it in your component:
const useIsFirstRender = () => {
const is_first_render = useRef(true);
useEffect(() => {
is_first_render.current = false;
}, []);
return is_first_render.current;
};
Then in your component:
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [updatesCount, setUpdatesCount] = useState(0);
const is_first_render = useIsFirstRender(); // Here
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
}, [props.title]);
useEffect(() => {
// I don't want this to trigger when the value is passed by the props (i.e. - when initialized)
if (is_first_render) { // Here
return;
}
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setUpdatesCount(updatesCount + 1);
}
}, [debouncedTitle, lastCommittedTitle, updatesCount]);
// ...
You can change the useDebounce hook to be aware of the fact that the first set debounce value should be set immediately. useRef is perfect for that:
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
const firstDebounce = useRef(true);
useEffect(() => {
if (value && firstDebounce.current) {
setDebouncedValue(value);
firstDebounce.current = false;
return;
}
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
I think you can improve your code in some ways:
First, do not copy props.title to a local state in ExampleTitleInput with useEffect, as it may cause excessive re-renders (the first for changing props, than for changing state as an side-effect). Use props.title directly and move the debounce / state management part to the parent component. You just need to pass an onChange callback as a prop (consider using useCallback).
To keep track of old state, the correct hook is useRef (API reference).
If you do not want it to trigger in the first render, you can use a custom hook, such as useUpdateEffect, from react-use: https://github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts, that already implements the useRef related logic.

Categories

Resources