Fixing hook call outside of the body of a function component - javascript

I made a custom ReactJS hook to handle a couple of specific mouse events, as below:
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const handleSelectedItem = (service: Filter) => {
service.selected = !service.selected;
setHealthcareServices([...healthcareServices]);
onChange(healthcareServices);
};
const handleSingleClick = (service: Filter) => {
console.log('single-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxSingleClick />;
}
handleSelectedItem(service);
};
const handleDoubleClick = (service: Filter) => {
console.log('double-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxDoubleClick />;
}
handleSelectedItem(service);
};
const handleClick = (service: Filter) =>
useSingleAndDoubleClick(
() => handleSingleClick(service),
() => handleDoubleClick(service)
);
...
return (
<div className={classes.filter_container}>
...
<div className={classes.filter_subgroup}>
{filters.map((filter) => (
<div key={`${filter.label}-${filter.value}`} className={classes.filter}>
<Checkbox
label={filter.label}
className={classes.checkbox}
checked={filter.selected}
onChange={() => handleClick(filter)}
checkedIcon={filter.checkedIcon}
/>
</div>
))}
</div>
...
</div>
);
};
When I click on my <Checkbox />, the whole thing crashes. The error is:
The top of my stacktrace points to useState inside my hook. If I move it outside, so the hook looks as:
const [click, setClick] = useState(0);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
The problem still happens, only the stacktrace points to the useEffect hook. The code is based on another answer here.
Any suggestions?

You've defined your useSingleAndDoubleClick hook inside of a component. That's not what you want to do. The idea of custom hooks is that you can move logic outside of your components that could otherwise only happen inside of them. This helps with code reuse.
There is no use for a hook being defined inside a function, as the magic of hooks is that they give you access to state variables and such things that are usually only allowed to be interacted with inside function components.
You either need to define your hook outside the component and call it inside the component, or remove the definition of useSingleAndDoubleClick and just do everything inside the component.
EDIT: One more note to help clarify: the rule that you've really broken here is that you've called other hooks (ie, useState, useEffect) inside your useSingleAndDoubleClick function. Even though it's called useSingleAndDoubleClick, it's not actually a hook, because it's not being created or called like a hook. Therefore, you are not allowed to call other hooks inside of it.
EDIT: I mentioned this earlier, but here's an example that could work of moving the hook definition outside the function:
EDIT: Also had to change where you call the hook: you can't call the hook in a nested function, but I don't think you need to.
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
// your other handlers
// changed this - don't call the hook inside the function.
// your hook is returning the handler you want anyways, I think
const handleClick = useSingleAndDoubleClick(handleSingleClick, handleDoubleClick)

Related

ResizeObserver API doesn't get the updated state in React

I am using ResizeObserver to call a function when the screen is resized, but I need to get the updated value of a state within the observer in order to determine some conditions before the function gets invoked.
It's something like this:
let [test, setTest] = React.useState(true)
const callFunction = () => {
console.log('function invoked')
setTest(false) // => set 'test' to 'false', so 'callFunction' can't be invoked again by the observer
}
const observer = React.useRef(
new ResizeObserver(entries => {
console.log(test) // => It always has the initial value (true), so the function is always invoked
if (test === true) {
callFunction()
}
})
)
React.useEffect(() => {
const body = document.getElementsByTagName('BODY')[0]
observer.current.observe(body)
return () => observer.unobserve(body)
}, [])
Don't worry about the details or why I'm doing this, since my application is way more complex than this example.
I only need to know if is there a way to get the updated value within the observer. I've already spent a considerable time trying to figure this out, but I couldn't yet.
Any thoughts?
The problem is, you are defining new observer in each re render of the component, Move it inside useEffect will solve the problem. also you must change this observer.unobserve(body) to this observer..current.unobserve(body).
I have created this codesandbox to show you how to do it properly. this way you don't need external variable and you can use states safely.
import { useEffect, useState, useRef } from "react";
const MyComponent = () => {
const [state, setState] = useState(false);
const observer = useRef(null);
useEffect(() => {
observer.current = new ResizeObserver((entries) => {
console.log(state);
});
const body = document.getElementsByTagName("BODY")[0];
observer.current.observe(body);
return () => observer.current.unobserve(body);
}, []);
return (
<div>
<button onClick={() => setState(true)}>Click Me</button>
<div>{state.toString()}</div>
</div>
);
};
export default MyComponent;

How handle long press event in react web

everyone! I use react and material ui library. I want to handle click event and long-press event separately. I think problem related to async set state, but for now, I don't know how to handle this events
const [isCommandHandled, setIsCommandHandled] = React.useState(null);
const handleButtonPress = function (e) {
setIsCommandHandled(false);
console.log('ON_MOUSE_DOWN ' + isCommandHandled); // here value null
buttonPressTimer = setTimeout(handleLongPress, 1500, e);
}.bind(this);
const handleLongPress = (e) => {
if (!isCommandHandled) {
setIsCommandHandled(true);
console.log('TIMER_IS_EXECUTED' + isCommandHandled); //Here value false or null
// some other logic for long press event
}
clearTimeout(buttonPressTimer);
};
const handleButtonRelease = function (e) {
if (!isCommandHandled) {//isCommandHandled isn't updated here, as a result logic is executed always
// got regular click, not long press
// specific logic
setIsCommandHandled(true);
}
clearTimeout(buttonPressTimer);
};
<IconButton
onMouseDown={(e) => handleButtonPress(e)}
onMouseUp={(e) => handleButtonRelease(e)}
>
```
You can use setState with callback and put the set timeout ID to state:
setIsCommandHandled((prevState)=>{
console.log("TIMER_IS_EXECUTED" + isCommandHandled); //Here value false or null
return true; });
Working Example:
https://codesandbox.io/s/material-demo-gc0le
This is how I handle a long press:
//import Hooks
import { useState, useEffect } from "react";
const Component = () => {
//pressState
const [pressed, setPressed] = useState();
//handleLongPress
useEffect(() => {
const timer = pressed
? setTimeout(() => {
console.log(pressed, "got pressed!");
}, 1300)
: null;
return () => clearTimeout(timer);
}, [pressed]);
//pressedElement
return (
<div
onMouseDown={(e) => setPressed(e.target)}
onMouseUp={() => setPressed()}
style={{ backgroundColor: "lightgreen" }}
>
Press me
</div>
);
};
export default Component;
Tested here: https://codesandbox.io/s/bold-bose-7vx3qg

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.

ReactJS Collapsible element breaks when setting new title and body in hooks

This is my first React project and I would like to use hooks, but I seem to have an issue with an element
function Document(props) {
const [id] = useState(props.match.params.id);
const [document, setDocument] = useState(0);
///////////////////////////////////////
useEffect(function getTheDocument() {
getDocument({
id,
}).then(document => {
setDocument(document.data);
}).catch(error => {
console.log(error);
});
}, [id]);
////////////////////////////////////////////
const [body, setBody] = useState(0);
const [title, setTitle] = useState(0);
////////////////////////
useEffect(function setBodyAndTitle() {
if (document) {
setTitle(document.title);
setBody(document.description);
}
}, [document]);
//////////////////////////
const changeBody = (data) => {
...
const module = ...
setTitle(module[0].title);
setBody(module[0].body);
}
}
So that is how I handle the body and title. When a button is clicked, changeBody is called which finds an object based on some values and sets a new title and body. But that component which is a simple collapsible menu like this. https://www.npmjs.com/package/react-collapse. The button is in the collapsible data. Am I handling this wrong? The menu is no longer collapsible all the hidden button can be seen. I expected the title and body to change ... that's all.
you don't need to handle states of title and body if these are a properties of document object. The same for id, but create an effect to do the request when id change and use state for document by setting a value for this state inside the effect, and create a state for collapse state.
Here is my code to handle this logic
import React from 'react'
function Document({ id = 1 }) {
const [document, setDocument] = React.useState({})
const [innerId, setId] = React.useState(() => id)
const [collapsed, setCollapsed] = React.useState(false)
React.useEffect(() => {
// Call my API request when my ID changes
fetch("https://jsonplaceholder.typicode.com/posts/"+innerId)
.then(response => response.json())
.then(setDocument)
console.log("rerender")
}, [innerId])
// For demo only, increment the ID every 5 seconds
React.useEffect(() => {
const intervalId = setInterval(() =>setId(i => i+1) , 5000)
return () => clearTimeout(intervalId)
}, [])
const toggle = React.useCallback(() => setCollapsed(c => !c))
// You can use useMemo hooks to get title and body if you want
const [title, body] => React.useMemo(
() => [document.title, document.body]
, [document]
)
return (<article>
<header>{document.title}</header>
<hr></hr>
<p style={{height: collapsed ? 0: "100%", overflow: "hidden"}}>
{document.body}
</p>
<button onClick={toggle}>Collapse</button>
</article>
);
}
ReactDOM.render(<Document id={1}/>, document.querySelector("#app"))
This is really cool. I'm beginning to like React. Seems that you can create a
memoized functional component https://logrocket.com/blog/pure-functional-components/ which doesn't update if the props don't change. In my case the props don't change at all after the initial render.
import React, { memo } from 'react';
function PercentageStat({ label, score = 0, total = Math.max(1, score) }) {
return (
<div>
<h6>{ label }</h6>
<span>{ Math.round(score / total * 100) }%</span>
</div>
)
}
function arePropsEqual(prevProps, nextProps) {
return prevProps.label === nextProps.label;
}
// Wrap component using `React.memo()` and pass `arePropsEqual`
export default memo(PercentageStat, arePropsEqual);

React Hooks multiple alerts with individual countdowns

I've been trying to build an React app with multiple alerts that disappear after a set amount of time. Sample: https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The problem is if I create multiple alerts, it disappears in the incorrect order. For example, test 0, test 1, test 2 should disappear starting with test 0, test 1, etc but instead test 1 disappears first and test 0 disappears last.
I keep seeing references to useRefs but my implementations don't resolve this bug.
With #ehab's input, I believe I was able to head down the right direction. I received further warnings in my code about adding dependencies but the additional dependencies would cause my code to act buggy. Eventually I figured out how to use refs. I converted it into a custom hook.
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
You have two things wrong with your code,
1) the way you use effect means that this function will get called each time the component is rendered, however obviously depending on your use case, you want this function to be called once, so change it to
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
adding the empty array as a second parameter, means that your effect does not depend on any parameter, and so it should only be called once.
Your delete alert depends on the value that was captured when the function was created, this is problematic since at that time, you don't have all the alerts in the array, change it to
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
here is your sample working after i forked it
https://codesandbox.io/s/multiple-alert-countdown-02c2h
well your problem is you remount on every re-render, so basically u reset your timers for all components at time of rendering.
just to make it clear try adding {Date.now()} inside your Alert components
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
you will notice the reset everytime
so to achieve this in functional components you need to use React.memo
example to make your code work i would do:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
2nd fix your useEffect to not run cleanup function on every render
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
finally something that is about taste, but really do you need to destruct the {...m} object ? i would pass it as a proper prop to avoid creating new object every time !
Both answers kind of miss a few points with the question, so after a little while of frustration figuring this out, this is the approach I came to:
Have a hook that manages an array of "alerts"
Each "Alert" component manages its own destruction
However, because the functions change with every render, timers will get reset each prop change, which is undesirable to say the least.
It also adds another lay of complexity if you're trying to respect eslint exhaustive deps rule, which you should because otherwise you'll have issues with state responsiveness. Other piece of advice, if you are going down the route of using "useCallback", you are looking in the wrong place.
In my case I'm using "Overlays" that time out, but you can imagine them as alerts etc.
Typescript:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
Then where the components are rendered:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
Basically: Each "overlay" has a unique ID. Each "overlay" component manages its own destruction, the overlay communicates back to the overlayManger via prop function, and then eslint exhaustive-deps is kept happy by setting an "alive" state property in the overlay component that, when changed to false, will call for its own destruction.

Categories

Resources