How to use State Variable (useState) in an EventHandler - reactjs

I have an React Functional Component which recieves a prop with a variable from useState(). This works fine but if I use it in an EventListener it does not get updated. I tried the following but still it does not work.
Maybe someone can explain why.
Thanks
I would expect x to be the updated number but it always has the value of the initial setup of the EventHandler.
import React, { useState, useEffect } from "react";
import ReactDom from "react-dom";
const App = () => {
const [num, setNum] = useState(50);
return (
<div>
<button onClick={() => setNum(prev => prev + 1)}>Test</button>
<div>{num}</div>
<Child num={num} />
</div>
);
};
const Child = ({ num }) => {
let x = 0;
const md = () => {
console.log(num, x);
};
useEffect(() => {
x = num;
}, [num]);
useEffect(() => {
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, []);
return <div id="box">click {num}</div>;
};
ReactDom.render(<App />, document.getElementById("app"));

Each render of your Child will get a new x, a new props object, etc. However you are binding your event listener only once and so capturing only the initial props.num value.
Two ways to fix:
Rebind event listener when num changes, by passing num as a dependency to your effect to bind the event listener:
const Child = ({ num }) => {
useEffect(() => {
// no need to define this in main function since it is only
// used inside this effect
const md = () => { console.log(num); };
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, [num]);
return <div id="box">click {num}</div>;
};
Or use a ref to hold the value of num and bind your event listener to the ref. This gives you a level of indirection to handle the change:
const Child = ({ num }) => {
const numRef = useRef(); // will be same object each render
numRef.current = num; // assign new num value each render
useEffect(() => {
// no need to define this in main function since it is only
// used inside this effect
// binds to same ref object, and reaches in to get current num value
const md = () => { console.log(numRef.current); };
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, []);
return <div id="box">click {num}</div>;
};

Related

Why does this memoised component passed as a prop remounts on every update?

sandbox here: https://codesandbox.io/s/heuristic-cache-rigddu
Basically I pass down a react function component and call it with certain props, one of which is a memoized function. Everytime it get's updated the passed component remounts, which breakes all transitions and animations.
export default function App() {
const [time, setTime] = useState(Date.now());
useEffect(() => {
const interval = setInterval(() => setTime(Date.now()), 500);
return () => clearInterval(interval);
}, []);
const getStyle = useCallback(
({ x, y }) => {
return { opacity: Math.sin((x + y) * (time / 5000)) };
},
[time]
);
const MemoPassed = useCallback(
(args) => {
return <Passed style={getStyle(args)} />;
},
[getStyle]
);
return <Grid countX={5} countY={5} Passed={MemoPassed} />;
}
Grid renders Cells further down the tree in which Passed is called:
export default function Cell({ x, y, Passed }) {
const passedProps = { x, y };
return (
<div>
<Passed {...passedProps} />
</div>
);
}
I managed to get it to work by calling getStyle in Cell component and passing it down, but that does not give me the elasticity I wanted. Also im curious why it does not work?

React State value not updated in Arrow functional component

React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);

How to bind context to React Functional Component

I have a function that I call from a child component callback. I'm trying to access some state variable but variables are undefined. I think the issue is when the child component callback the function context it not bind to the parent component. How to do this.
It is sure that myVariable is set before myFunciton is called.
const MyParentView = props => {
const[myVariable, setMyVariable] = useState(undefined)
const onTextFieldChange = val => {
setMyVariable(val)
}
const myFunction = () => {
// myVariable is set to some value by this time
console.log(myVariable)
// But it logs undefined
}
return (
<Input onChange={e => onTextFieldChange(e.target.value)}
<MyChildComponent getData={()=>myFunction()}/>
)
}
Following is the child component ( The actual one )
// #flow
import React, { useEffect, useRef } from "react"
import { get } from "lodash"
type Props = {
children: any,
getData?: Function,
threshold?: number
}
const InfiniteScroll = ({ children, getData, threshold = 0.9 }: Props) => {
const listRef = useRef()
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
}, [])
useEffect(() => {
if (listRef.current) {
const bottom = listRef.current.getBoundingClientRect().bottom
const height =
window.innerHeight || get(document, "documentElement.clientHeight")
if (bottom <= height) {
getData && getData()
}
}
})
const handleScroll = () => {
const winScroll =
get(document, "body.scrollTop") ||
get(document, "documentElement.scrollTop")
const height =
get(document, "documentElement.scrollHeight") -
get(document, "documentElement.clientHeight")
const scrolled = winScroll / height
if (scrolled >= threshold) {
getData && getData()
}
}
return <div ref={listRef}>{children}</div>
}
export default InfiniteScroll
Try returning a closure in your myFunction like this:
const myFunction = () => {
return function() {
// myVariable is set to some value by this time
console.log(myVariable)
// But it logs undefined
}
}

React Sequential Rendering Hook

I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};

React custom Hook using useRef returns null for the first time the calling component Loads?

I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}

Resources