I have a framer motion component (I've tried with Stitches components too) that animates to and from view based on a useState variable:
const [visible, setVisible] = useState(true);
<motion.div
animate={visible ? "one" : "two"}
variants={{
one: {
transform: "translateX(0%)",
opacity: 1
},
two: {
transform: "translateX(100%)",
opacity: 0
}
}}
>
// children
</motion.div>
The children components are wrapped in contexts, and the problem is that their states reset every time the animation is triggered.
I've tried declaring the animation component outside of its current scope. In its current scope it resets the states of everything except the "currentUser" state in my AuthProvider context. Outside the scope it resets "currentUser" too. I've decided not to include the original code for brevity.
Here is a minimum reproducible. If you type something in the input, and then click the toggle button, the input is lost.
DummyInputComponent should be stateless, use the value provided by the context and also have a onChange handler function passed on from the context
Something like this:
export function AuthProvider({ children }) {
const [dummyValue, setDummyValue] = useState(null);
const handlechange = (e) => {
setDummyValue(e.target.value);
};
const value = {
dummyValue,
onChange: handlechange
};
const DummyInputComponent = () => {
return <input value={value.dummyValue || ""} onChange={value.onChange} />;
};
return (
<AuthContext.Provider value={value}>
<DummyInputComponent />
</AuthContext.Provider>
);
}
Working CodeSandbox
I know the common use case for this is for performance tweaking, but I ended up avoiding the rerenders caused by framer motion (supposedly) by using memoization:
const Memoized = React.memo(({ children }) => {
const [visible, setVisible] = useState(true);
return (
<motion.div
animate={visible ? "one" : "two"}
variants={{
one: {
transform: "translateX(0%)",
opacity: 1
},
two: {
transform: "translateX(100%)",
opacity: 0
}
}}
>
{children}
</motion.div>
);
});
I don't know if this is a good way to do it, so I'll leave this open.
Related
I have three components First, Second and Third that need to render one after the other.
My App looks like this at the moment:
function App() {
return (
<First/>
)
}
So ideally, there's a form inside First that on submission (onSubmit probably) triggers rendering the Second component, essentially getting replaced in the DOM. The Second after some logic triggers rendering the Third component and also passes a value down to it. I'm not sure how to go on about it.
I tried using the useState hook to set a boolean state to render one of the first two components but I would need to render First, then somehow from within it change the set state in the parent which then checks the boolean and renders the second. Not sure how to do that. Something like below?
function App() {
const { isReady, setIsReady } = useState(false);
return (
isReady
? <First/> //inside this I need the state to change on form submit and propagate back up to the parent which checks the state value and renders the second?
: <Second/>
);
}
I'm mostly sure this isn't the right way to do it.
Also need to figure out how to pass the value onto another component at the time of rendering it and getting replaced in the DOM. So how does one render multiple components one after the other on interaction inside each? A button click for example?
Would greatly appreciate some guidance for this.
then somehow from within it change the set state in the parent which then checks the boolean and renders the second.
You're actually on the right track.
In React, when you're talking about UI changes, you have to manage some state.
So we got that part out of the way.
Now, what we can do in this case is manage said state in the parent component and pass functions to the children components as props in-order to allow them to control the relevant UI changes.
Example:
function App() {
const { state, setState } = useState({
isFirstVisible: true,
isSecondVisible: false,
isThirdVisible: false,
});
const onToggleSecondComponent = (status) => {
setState(prevState => ({
...prevState,
isSecondVisible: status
}))
}
const onToggleThirdComponent = (status) => {
setState(prevState => ({
...prevState,
isThirdVisible: status
}))
}
return (
{state.isFirstVisible && <First onToggleSecondComponent={onToggleSecondComponent} /> }
{state.isSecondVisible && <Second onToggleThirdComponent={onToggleThirdComponent} /> }
{state.isThirdVisible && <Third/> }
);
}
Then you can use the props in the child components.
Example usage:
function First({ onToggleSecondComponent }) {
return (
<form onSubmit={onToggleSecondComponent}
...
</form
)
}
Note that there are other ways to pass these arguments.
For example, you can have one function in the parent comp that handles them all, or you can just pass setState to the children and have them do the logic.
Either way, that's a solid way of achieving your desired outcome.
Seen as your saying there are stages, rather than having a state for each stage, just have a state for the current stage, you can then just increment the stage state to move onto the next form.
Below is a simple example, I've also used a useRef to handle parent / child state, basically just pass the state to the children and the children can update the state. On the final submit I'm just JSON.stringify the state for debug..
const FormContext = React.createContext();
const useForm = () => React.useContext(FormContext);
function FormStage1({state}) {
const [name, setName] = React.useState('');
state.name = name;
return <div>
Stage1:<br/>
name: <input value={name} onChange={e => setName(e.target.value)}/>
</div>
}
function FormStage2({state}) {
const [address, setAddress] = React.useState('');
state.address = address;
return <div>
Stage2:<br/>
address: <input value={address} onChange={e => setAddress(e.target.value)}/>
</div>
}
function FormStage3({state}) {
const [hobbies, setHobbies] = React.useState('');
state.hobbies = hobbies;
return <div>
Stage3:<br/>
hobbies: <input value={hobbies} onChange={e => setHobbies(e.target.value)}/>
</div>
}
function Form() {
const [stage, setStage] = React.useState(1);
const state = React.useRef({}).current;
let Stage;
if (stage === 1) Stage = FormStage1
else if (stage === 2) Stage = FormStage2
else if (stage === 3) Stage = FormStage3
else Stage = null;
return <form onSubmit={e => {
e.preventDefault();
setStage(s => s + 1);
}}>
{Stage
? <React.Fragment>
<Stage state={state}/>
<div>
<button>Submit</button>
</div>
</React.Fragment>
: <div>
{JSON.stringify(state)}
</div>
}
</form>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Form/>);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
For context, I have a web app that displays an image in a React-Bootstrap Container component (Arena) that holds an image where users are to look and find specific characters.
Separately, I created a div component (CustomCursor) where the background is set to a magnifying glass SVG image.
The Arena component tracks mouse position through an OnMouseMove handler function (handleMouseMove) and passes those coordinates as props to the CustomCursor component.
Here is my Arena component code:
import { useState, useEffect } from 'react';
import { Container, Spinner } from 'react-bootstrap';
import CustomCursor from '../CustomCursor/CustomCursor';
import Choices from '../Choices/Choices';
import { getImageURL } from '../../helpers/storHelpers';
import './Arena.scss';
export default function Arena(props) {
const [arenaURL, setArenaURL] = useState('');
const [loaded, setLoaded] = useState(false);
const [clicked, setClicked] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function handleClick(e) {
setClicked(true);
}
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
useEffect(() => {
retreiveArena();
// FUNCTION DEFINITIONS
async function retreiveArena() {
const url = await getImageURL('maps', 'the-garden-of-earthly-delights.jpg');
setArenaURL(url);
setLoaded(true);
}
}, [])
return (
<Container as='main' fluid id='arena' className='d-flex flex-grow-1 justify-content-center align-items-center' onClick={handleClick}>
{!loaded &&
<Spinner animation="border" variant="danger" />
}
{loaded &&
<img src={arenaURL} alt='The Garden of Earthly Delights triptych' className='arena-image' onMouseMove={handleMouseMove} />
}
{clicked &&
<Choices x={x} y={y} />
}
<CustomCursor x={x} y={y} />
</Container>
)
}
Here is my CustomCursor code:
import './CustomCursor.scss';
export default function CustomCursor(props) {
const { x, y } = props;
return (
<div className='custom-cursor' style={{ left: `${x - 64}px`, top: `${y + 50}px` }} />
)
}
When I first created the OnMouseMove handler function I simply set the x and y state values by passing them into their respective state setter functions directly:
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
However, I noticed this was slow and laggy and when I refactored this function to use setter functions instead it was much faster (what I wanted):
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
Before:
After:
Why are using setter functions faster than passing in values directly?
This is interesting. First of all, we need to focus on reacts way of updating state. In the documentation of react https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous There you can see:
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
For example, this code may fail to update the counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
A pretty good article on this is written by Jan Hesters here:
https://medium.com/#jan.hesters/updater-functions-in-reacts-setstate-63c7c162b16a
And more details here:
https://learn.co/lessons/react-updating-state
I have a global variable plyViewed in App.js that is set outside of my App component.
let plyViewed = 0;
function App() {
It monitors which move the board game is on. Below the board are some navigation buttons. If you click < I do plyViewed--, if you click I do plyViewed++. You get the picture.
This all worked fine, until I refactored!
I took the navigation buttons, who’s JSX code was all inside the App() function and put it in <MoveButtons /> in MoveButtons.js. So, I thought I could pass down plyViewed as a prop and then update the value in my code in the MoveButton child component. Then I find that props are immutable! Now I am stuck.
My code below gives an example of how I am using that plyViewed code. When someone clicks the navigation buttons, it fires an event that triggers the code to update plyViewed, although now it doesn’t work anymore because it is a prop. The rest of the game data is stored in an object called gameDetails.
I am passing down the plyViewed like this:
<MoveButtons
plyViewed={plyViewed}
// etc.
/>
A shortened version of my MoveList component is below.
plyViewed is used in multiple areas throughout my app, like gameDetails. I’ve considered putting it in the gameDetails object, but then I still have the issue of gameDetails being immutable if passed down as a prop. Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
Am I thinking about this all wrong?
export default function MoveButtons(props) {
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);
}
you are trying to update the props that are passed down from the higher-level component in your component tree, which is not possible.
You have the option to create a state using React's useState hook and passing down both the value and the dispatcher, but this is not recommended because you would be drilling props down the tree.
You can also pass the onClick events (or parts of them), up to your App component, which is an improvement to the first method but not the best practice in your case.
What you should really be doing is managing your global state using either, React's own Context API, or Redux. I think this could help you out.
While we're missing the full picture, it sounds like plyViewed should be a state and the asynchronous behaviour shouldn't prevent any computation if done properly with React.
It's easy to overlook the fact that the new state value is synchronously computed by ourselves when setting the state. We can just use that same local value to compute anything else and the async behaviour isn't affecting us at all.
onClick={() => {
if (props.plyViewed > 0) {
// New local value computed by ourselves synchronously.
const updatedPlyViewed = props.plyViewed - 1;
// Set the state with the new value to reflect changes on the app.
props.setPlyViewed(updatedPlyViewed);
// Use the up-to-date local value to compute anything else
props.board.current.setPosition(props.fenHistory[updatedPlyViewed]);
props.setFen(props.fenHistory[updatedPlyViewed]);
props.setSelectedIndex(updatedPlyViewed);
}
}}
This is a really simple pattern that should help solve the most basic issues with new state values.
Simple computations
Quick computations can be done in the render phase. The latest state values will always be available at this point. It's unnecessary to sync multiple state values if it can easily be computed from a single value, like the plyViewed here.
const [plyViewed, setPlyViewed] = useState(0);
// No special state or function needed to get the position value.
const position = fenHistory[plyViewed];
Here's an interactive example of how a simple state can be used to compute a lot of different derived information within the render phase.
// Get a hook function
const { useState } = React;
// This component only cares about displaying buttons, the actual logic
// is kept outside, in a parent component.
const MoveButtons = ({ onBack, onNext }) => (
<div>
<button type="button" onClick={onBack}>
Back
</button>
<button type="button" onClick={onNext}>
Next
</button>
</div>
);
const App = () => {
const [fenHistory, setFenHistory] = useState(["a", "b"]);
const [plyViewed, setPlyViewed] = useState(0);
const position = fenHistory[plyViewed];
const onBack = () => setPlyViewed((curr) => Math.max(curr - 1, 0));
const onNext = () =>
setPlyViewed((curr) => Math.min(curr + 1, fenHistory.length - 1));
return (
<div>
<p>Ply viewed: {plyViewed}</p>
<p>Fen position: {position}</p>
<p>Fen history: {fenHistory.join(", ")}</p>
<button
type="button"
onClick={() =>
setFenHistory((history) => [...history, `new-${history.length}`])
}
>
Add to fen history
</button>
<MoveButtons onBack={onBack} onNext={onNext} />
</div>
);
};
// Render it
ReactDOM.render(<App />, document.getElementById("app"));
button {
margin-right: 5px;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Expensive computations
If the computation takes a considerable amount of time to complete, and that doing it each render cycle is noticeably slowing down the rendering, there are some optimizations we could do.
useMemo will only recompute the memoized value when one of the
dependencies has changed. This optimization helps to avoid expensive
calculations on every render.
const [plyViewed, setPlyViewed] = useState(0);
const position = useMemo(() => /* costly computation here */, [plyViewed]);
Complex computations
If the computation has a lot of dependencies, we could use useReducer to manage a state object.
Note that the following example isn't justifying the use of useReducer and it's only used as an example of the implementation.
const initialState = {
plyViewed: 0,
fenHistory: ["a", "b"],
positionValue: "a",
};
function reducer(state, action) {
const { plyViewed, fenHistory } = state;
switch (action.type) {
case "back":
if (fenHistory.length <= 0) return state;
const newIndex = plyViewed - 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "next":
if (fenHistory.length - 1 > plyViewed) return state;
const newIndex = plyViewed + 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "add":
return {
...state,
fenHistory: [...fenHistory, action.value],
};
default:
throw new Error();
}
}
const App = () => {
const [{ plyViewed, fenHistory, positionValue }, dispatch] = useReducer(
reducer,
initialState
);
const onBack = () => dispatch({ type: "back" });
const onNext = () => dispatch({ type: "next" });
const onAdd = () => dispatch({ type: "add", value: 'anything' });
// ...
Async computations
If we need to get the result, for example, from a distant server, then we could use useEffect which will run once when the value changes.
const App = () => {
const [plyViewed, setPlyViewed] = useState(0);
const [position, setPosition] = useState(null);
useEffect(() => {
fetchPosition(plyViewed).then((newPosition) => setPosition(newPosition));
}, [plyViewed]);
There are a couple pitfalls with useEffect and asynchronously setting the state.
Prevent setting state on an unmounted component.
plyViewed may have changed again since the first fetch was triggered but before it actually succeeded, resulting in a race-condition
Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
I think this is incorrect, you just need to start using useEffect as well:
function App() {
const [plyViewed, setPlyViewed] = useState(0);
<MoveButtons
plyViewed={plyViewed}
setPlyViewed={setPlyViewed}
/>
}
export default function MoveButtons(props) {
useEffect(() => {
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}, [props.plyViewed, props.fenHistory]);
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);
I have a section with a fixed height. I don't know when the component mounts (first renders) whether the content coming in will fit or not. If it does NOT fit, then I need to render a 'Read More' button.
It looks like this:
I wrote this originally as a Class component using the lifecycle methods DidMount/DidUpdate:
Class Component
import React, { createRef } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
class ParagraphList extends React.Component {
state = {
overflowActive: false,
}
wrapper = createRef() // so we can get a ref to the height container
isOverflowing(el) {
if (el) return el.offsetHeight < el.scrollHeight
}
componentDidMount() {
this.setState({ overflowActive: this.isOverflowing(this.wrapper.current) })
}
componentDidUpdate() {
if (this.wrapper.current && !this.state.overflowActive) {
this.setState({
overflowActive: this.isOverflowing(this.wrapper.current),
})
}
}
handleClick() {
this.setState({ overflowActive: false })
}
render() {
const { moreButtonText, titleText, paragraphs, theme } = this.props
return (
<>
<Section overflowActive={this.state.overflowActive}>
{this.state.overflowActive || !this.wrapper.current ? (
<StyledHeightContainer ref={this.wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(this)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
}
export default ParagraphList
My best way to explain the flow:
When the component mounts, the flag is false and we have no reference to the div so the StyledHeightContainer will try to render and thus provide a ref to it
In componentDidMount -> try to set the overflow flag (which will be false because at this point we do not yet have rendering completed so the ref will be null). But by setting the flag anyway, we queue an additional render pass
1st INITIAL rendering completes -> we have a ref to the div now
The 2nd (queued) render occurs, firing the componentDidUpdate -> we can calculate the overflow and set the flag to true when the content overflows
When the user clicks the button -> set the flag to false, which will trigger a re-render and hence the StyledHeightContainer will be removed from the DOM.
Functional Component With Hooks
Sandbox of the code
When I re-wrote this as a functional component using Hooks, I ended up with this:
import React, { createRef, useEffect, useState } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
const ParagraphList = ({ moreButtonText, titleText, paragraphs, theme }) => {
const [overflowActive, setOverflowActive] = useState(false)
const [userClicked, setUserClicked] = useState(false)
const wrapper = createRef(false) // so we can get a ref to the height container
const isOverflowing = el => {
if (el) return el.offsetHeight < el.scrollHeight
}
useEffect(() => {
if (!userClicked && !overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
}, [userClicked]) // note: we only care about state change if user clicks 'Read More' button
const handleClick = () => {
setOverflowActive(false)
setUserClicked(true)
}
return (
<>
<Section theme={theme} overflowActive={overflowActive}>
{!userClicked && (overflowActive || !wrapper.current) ? (
<StyledHeightContainer ref={wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(null)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
export default ParagraphList
I was surprised that I needed to add another state (userClicked), which is how I force the 2nd render to occur (ie. the equivalent to the componentDidUpdate in the class solution).
Is this correct or can someone see a more concise way to write the 2nd solution?
NOTE
One of the reasons I ask is because in the console I get this warning:
48:6 warning React Hook useEffect has missing dependencies:
'overflowActive' and 'wrapper'. Either include them or remove the
dependency array react-hooks/exhaustive-deps
and I don't THINK I want to add them to the dependency array, as I don't want to trigger rendering when they change...?
I really enjoyed while solving the query.
Here is the implementation: https://codesandbox.io/s/react-using-hooks-in-section-component-5gibi?file=/src/ParagraphList.js
First of all, I was thinking of
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, [wrapper]);
But if we do this, it will again call the useEffect as when we'll click on the Read more button. Because it was comparing the reference of the wrapper and not it's value.
So, to avoid the reference comparison we have to use the useCallback hook.
const isOverflowingNode = node => {
return node.offsetHeight < node.scrollHeight;
};
const wrapper = useCallback(node => {
if (node !== null) {
setOverflowActive(isOverflowingNode(node));
}
}, []);
I came across the beautiful discussion: https://github.com/facebook/react/issues/14387
For more information:
https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
Thanks for the question :)
You could add an extra useEffect(() => (...),[]) that acts like componentDidMount(). And another useEffect(() => (...)) that acts like componentDidUpdate(). Then you should be able to get rid of userClicked.
This is a good link on how the lifestyle methods work with hooks. https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, []);
useEffect(() => {
if (!overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
});
The second one might need to be useLayoutEffect if you are wanting the update to happen after the layout.
I am attempting to add and remove an event listener within a functional React component. The listener is added fine but is not removed when asked to be. I believe the issue is that the function I am referencing handlemousemove is recreated every component render and so when removeEventListener attempts to remove it, it's not the same function reference as when addEventListener added it.
I tried moving handlemousemove out of the component but it required access to the setState hooks generated in the component.
const handleMouseMove = e => {
setYOffset(e.clientY-280)
setXOffset(e.clientX-350)
}
const followMouse = () => {
if (isFollowingMouse){
setIsFollowingMouse(false)
document.removeEventListener("mousemove", handleMouseMove)
} else {
setIsFollowingMouse(true)
document.addEventListener("mousemove", handleMouseMove)
}
}
...
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
All branches of execution are hit here but document.removeEventListener("mousemove", handleMouseMove) doesn't actually remove the event listener.
Is there a way to have a "static method" within a functional component? Is that even the issue here?
Here's a link to code sandbox with the whole code: https://codesandbox.io/s/pzrwh
The old way to do it was with render props, but now that hooks have arrived this is a better solution
const MyComponent = (props) => {
const [isFollowingMouse, setIsFollowingMouse] = React.useState(false);
const [xOffset, setXOffset] = React.useState(0);
const [yOffset, setYOffset] = React.useState(0);
const handleMouseMove = e => {
if (isFollowingMouse) {
setYOffset(e.clientY-28);
setXOffset(e.clientX-35);
}
};
const followMouse = () => {
setIsFollowingMouse(!isFollowingMouse);
}
const styles = {
'cat': {
'backgroundColor': 'red',
'height': '20px',
'position': 'absolute',
'left': xOffset,
'top': yOffset,
'width': '20px',
'display': isFollowingMouse ? 'block' : 'none'
}
};
return (
<div style={{ 'height': '100%' }} onMouseMove={handleMouseMove}>
<div style={ styles.cat }>C</div>
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
</div>
)
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
html,
body,
#root {
height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I think your description of the issue is spot on. A quick fix is to define the variable handleMouseMove outside of your App function - essentially making the variable static and not recreated every render.
Then, within the body of the function, only assign the handleMouseMove variable if it's currently unassigned, and set it back to null when you set isFollowingMouse to false.
With React 16.7 you can use Hooks to do this:
import React, { useCallback, useEffect, useState } from 'react';
const DraggedComponent = React.memo(
props => {
const [isFollowingMouse, setIsFollowingMouse] = useState(false);
const [xOffset, setXOffset] = useState(0);
const [yOffset, setYOffset] = useState(0);
const handleMouseMove = useCallback(
e => {
if (isFollowingMouse) {
setYOffset(e.clientY-28);
setXOffset(e.clientX-35);
}
}, [isFollowingMouse, setYOffset, setXOffset]
);
useEffect(
() => {
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
},
[handleKeyDown]
);
const followMouse = () => setIsFollowingMouse(!isFollowingMouse);
return (
<div onMouseMove={handleMouseMove}>
<div>C</div>
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
</div>
)
}
);
ReactDOM.render(<DraggedComponent />, document.getElementById('root'));
In this example React.memo() ensures that the component is only redrawn if state or properties change. Similar useCallback() will cache the event listener for the mousemove event, such that this will not be recreated only if isFollowingMouse, setYOffset or setXOffset change, instead of every rerender. useEffect will be called once the component is created, and once every time the handleMouseMove callback changes. Furthermore it returns a function, which is automatically called if the component is destroyed or the parameter handleKeyDown changes.