I'm trying to dynamically pass the width to a component's styles. On first load, it's okay, but if I resize it never re renders the component, even though the hook is working.
I read about that since NextJs is server side rendering this can cause this client side's issues. So here's the code:
Hook
const useWidth = () => {
if (process.browser) {
const [width, setWidth] = useState(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [width]);
return width;
}
return 0;
};
Component (reduced just to show the example)
const Login = () => {
const windowWidth = useWidth();
const width = windowWidth > CELLPHONE_WIDTH ? '36.6rem' : '90%';
const loginStyles = styles(width);
return (
<div className='container'>
<TextInput
type='text'
width={width}
placeholder='Email'
/>
</div>
);
};
Styles
function textInputStyles(width) {
return css`
width: ${width};
`;
}
export default textInputStyles;
Problem here is the code first runs on server side with Next.js. Because process.browser returns false on the server side, your hook logic is never registered. Only a 0 is returned. Since no hook has been registered and no event has been set, changing window size will not trigger a re-render.
You need to use a componentDidMount() or a useEffect.
Here is an example for your case that would work.
const useWidth = () => {
const [width, setWidth] = useState(0); // default width, detect on server.
const handleResize = () => setWidth(window.innerWidth);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
return width;
};
On the other hand, if you want to ensure that your initial state is that of the browser window, you can load your component dynamically on the client side only.
import dynamic from 'next/dynamic'
const Login = dynamic(
() => import('./pathToLogin/Login'),
{ ssr: false },
)
and in your component where Login is used.
const TopLevelComponent = () => {
<Login {...props} />
}
and then you can use the window object freely in your Login component.
const useWidth = () => {
// Use window object freely
const [width, setWidth] = useState(window.innerWidth); // default width, detect on server.
Refer to this if there is still confusion.
Thanks a lot Hassaan Tauqir for your help!!! :D
When I saw your first answer I tried it but couldn't call the custom hook inside useEffect because it was breaking the rule Call Hooks from React function components
But I managed to achieve the solution with this code, that is almost the same as the one you posted after you edited the answer. The only difference is that in the dependencies array of the useEffect inside the custom hook im using width instead of the handler. Dunno if that makes any difference in this case but its working perfectly.
const useWidth = () => {
const [width, setWidth] = useState(0);
const handleResize = () => setWidth(window.innerWidth);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [width]);
return width;
};
And from the component Im using it like:
const Login = () => {
const [width, setWidth] = useState('0');
const windowWidth = useWidth();
useEffect(() => {
if (windowWidth < CELLPHONE_WIDTH) {
setWidth('90%');
} else {
setWidth('36.6rem');
}
}, []);
// rest of the code
Related
My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can't figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
function useIsInViewport(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) =>
setIsIntersecting(entry.isIntersecting)
),
[]
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, observer]);
return isIntersecting;
}
var videoProxy = new Proxy(videoViewports, {
set: function (target, key, value) {
// console.log("value " + value)
// console.log("key " + key)
console.log(videoViewports);
if (value) {
setMostRecentVideo(key);
//console.log("Most Rec: " + mostRecentVideo);
}
target[key] = value;
return true;
},
});
const [isGlobalMute, setIsGlobalMute] = useState(true);
const [mostRecentVideo, setMostRecentVideo] = useState("");
videoProxy["Podcast 1"] = useIsInViewport(ref1);
videoProxy["Podcast 2"] = useIsInViewport(ref2);
videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer
ref={ref1}
videoProxy={videoProxy}
mostRecentVideo={mostRecentVideo}
setMostRecentVideo={setMostRecentVideo}
title="Podcast 1"
isGlobalMute={isGlobalMute}
setIsGlobalMute={setIsGlobalMute}
videoSource={video1}
podcastName={podcastName}
networkName={networkName}
episodeName={episodeName}
episodeDescription={episodeDescription}
logo={takeLogo}
muteIcon={muteIcon}
unmuteIcon={unmuteIcon}
></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don't understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can't be wrapped in a useEffect(() => {}, []) hook. Maybe I'm doing this completely the wrong way with the wrong React Hooks, but I've been bashing my head against this for so long...
First of all: I'm not quite sure, if a Proxy.set is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I've created a simple solution that uses two components. First the a VideoList that contains all videos and manages the viewport calculations so you don't have thousands of event listeners on resize, scroll and so on (or Observers respectively).
The Video component is a forwardRef component, so we get the ref of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react";
function inViewport(el) {
if (!el) {
return false;
}
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const Video = forwardRef((props, ref) => {
return (
<div ref={ref}>
<p>{props.source}</p>
<video {...props} />
</div>
);
});
const VideoList = ({ sources }) => {
const sourcesLength = sources.length;
const [refs, setRefs] = useState([]);
useEffect(() => {
// set refs
setRefs((r) =>
Array(sources.length)
.fill()
.map((_, i) => refs[i] || createRef())
);
}, [sourcesLength]);
const isInViewport = useCallback(() => {
// this returns only the first but you can also apply a `filter` instead of the index
const videoIndex = refs.findIndex((ref) => {
return inViewport(ref.current);
});
if (videoIndex < 0) {
return;
}
console.log(`lastSeen video is ${sources[videoIndex]}`);
}, [refs, sources]);
useEffect(() => {
// add more listeners like resize, or use observer
document.addEventListener("scroll", isInViewport);
document.addEventListener("resize", isInViewport);
return () => {
document.removeEventListener("scroll", isInViewport);
document.removeEventListener("resize", isInViewport);
};
}, [isInViewport]);
return (
<div>
{sources.map((source, i) => {
return <Video ref={refs[i]} source={source} key={i} />;
})}
</div>
);
};
export default function App() {
const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"];
return (
<div className="App">
<VideoList sources={sources} />
</div>
);
}
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I'm using a useState for the ref list.
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)
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;
I have width in state which changes with window resize and showFilters as props which changes from true to false. And I want to remove listener on unmount. So, I have used three useState for each these conditions.
So, is there any refactor I can do to use all these in single useEffect.
import React, { useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { Icon } from 'antd'
import TrendsChart from './trendsChart'
import styled from '../styled-components'
const Chart = ({ showFilters }) => {
const [width, setWidth] = useState(null)
useEffect(() => {
window.addEventListener('resize', handleWindowResize)
updateWidth()
}, [width])
useEffect(() => {
setTimeout(updateWidth, 200)
}, [showFilters])
useEffect(() => () => {
window.removeEventListener('resize', handleWindowResize)
})
const updateWidth = () => {
const containerWidth = chartRef.current.clientWidth
setWidth(Math.floor(containerWidth))
}
const handleWindowResize = () => {
updateWidth()
}
const chartRef = useRef()
function render() {
return (
<styled.chart>
<styled.chartHeader>
Daily
</styled.chartHeader>
<styled.trendsChart id="chartRef" ref={chartRef}>
<TrendsChart width={width} showFilters={showFilters}/>
</styled.trendsChart>
<div>
<Icon type="dash" /> Credit Trend
</div>
</styled.chart>
)
}
return (
render()
)
}
Chart.propTypes = {
showFilters: PropTypes.bool.isRequired
}
export default Chart
from what i understand is
two of your useEffect can be merge
useEffect(() => {
window.addEventListener('resize',handleWindowResize)
return () => window.removeEventListener('resize',handleWindowResize)
},[width])
For the set timeout part, from what i understand. That is not needed because react will rerender everytime the width(state) is changed. Hope it is help. I'm new to react too.
You should look at CONDITIONS, when each effect works.
Listener should be installed ONCE, with cleanup:
useEffect(() => {
window.addEventListener('resize',handleWindowResize)
return () => window.removeEventListener('resize',handleWindowResize)
},[])
const handleWindowResize = () => {
const containerWidth = chartRef.current.clientWidth
setWidth(Math.floor(containerWidth))
}
NOTICE: [] as useEffect parameter, without this effect works on every render
... and it should be enough as:
handleWindowResize sets width on window size changes;
showFilters causes rerender automatically
I'm dynamically rendering a list of Symbol(react.element) by mapping into an array and placing each of its elements HTML tags. My question is therefore: how can I get the height of each of the rendered Symbol(react.element)? This seems not to be in the Symbol(react.element)'s object.
Thanks in advance for your help
Actually, if you are using Functional Components, would be better to isolate this resize logic in a custom hook instead of leave it inside the component. You can create a custom hook like this:
const useResize = (myRef) => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleResize = () => {
setWidth(myRef.current.offsetWidth)
setHeight(myRef.current.offsetHeight)
}
useEffect(() => {
myRef.current && myRef.current.addEventListener('resize', handleResize)
return () => {
myRef.current.removeEventListener('resize', handleResize)
}
}, [myRef])
return { width, height }
}
and then you can use it like:
const MyComponent = () => {
const componentRef = useRef()
const { width, height } = useResize(componentRef)
return (
<div ref={componentRef}>
<p>width: {width}px</p>
<p>height: {height}px</p>
<div/>
)
}
class MyComponent extends Component {
constructor(props){
super(props)
this.myDiv = React.createRef()
}
componentDidMount () {
console.log(this.myDiv.current.offsetHeight)
}
render () {
return (
<div ref={this.myDiv}>element</div>
)
}
}
A modified version of Marcos answer.
I've placed a rendering bool to make sure all data is rendered before placing the height and width. This is to be sure that the height is calculated with all required elements in place instead of risking receiving an incorrect height and width.
useResize hook placed in a separate folder:
import { useState, useEffect, useCallback } from 'react';
export const useResize = (myRef: React.MutableRefObject<any>, rendering: boolean) => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const handleResize = useCallback(() => {
setWidth(myRef.current.offsetWidth);
setHeight(myRef.current.offsetHeight);
}, [myRef]);
useEffect(() => {
if (!rendering) {
myRef.current && myRef.current.addEventListener('resize',
handleResize(), { once: true });
}
}, [myRef, handleResize, rendering]);
return { width, height };
Example of usage:
const MyComponent = ({ A, B }) => {
// A and B is data that is required in component
const componentRef = useRef()
const { width, height } = useResize(componentRef, !A || !B)
if (!A || !B) return;
return (
<div ref={componentRef}>
<p>{A} {width}px</p>
<p>{B} {height}px</p>
<div/>
)
}
const componentRef = useRef(null)
and div ref={componentRef}